如何在C#中强制使用方法的返回值?

时间:2011-03-23 20:05:27

标签: c# compiler-construction attributes lazy-loading return-value

我有一个用流利的语法编写的软件。方法链有一个明确的“结束”,之前在代码中实际上没有任何有用的东西(想想NBuilder,或者Linq-to-SQL的查询生成实际上并没有实际访问数据库,直到我们用ToList()迭代我们的对象。 )。

我遇到的问题是其他开发人员对正确使用代码存在困惑。他们忽略了称之为“结束”的方法(从而实际上从未“做任何事情”)!

我对强制使用某些方法的返回值感兴趣,这样我们就不会在不调用“Finalize()”或“Save()”的情况下“结束链”实际完成工作的方法。

请考虑以下代码:

//The "factory" class the user will be dealing with
public class FluentClass
{
    //The entry point for this software
    public IntermediateClass<T> Init<T>()
    {
        return new IntermediateClass<T>();
    }
}

//The class that actually does the work
public class IntermediateClass<T>
{
    private List<T> _values;

    //The user cannot call this constructor
    internal IntermediateClass<T>()
    {
        _values = new List<T>();
    }

    //Once generated, they can call "setup" methods such as this
    public IntermediateClass<T> With(T value)
    {
        var instance = new IntermediateClass<T>() { _values = _values };
        instance._values.Add(value);
        return instance;
    }

    //Picture "lazy loading" - you have to call this method to
    //actually do anything worthwhile
    public void Save()
    {
        var itemCount = _values.Count();
        . . . //save to database, write a log, do some real work
    }
}

正如您所看到的,正确使用此代码将类似于:

new FluentClass().Init<int>().With(-1).With(300).With(42).Save();

问题在于人们正在以这种方式使用它(认为它与上述相同):

new FluentClass().Init<int>().With(-1).With(300).With(42);

这个普遍存在的问题是,出于完全良好的意图,另一位开发人员实际上改变了“Init”方法的名称,以表明该方法正在进行软件的“实际工作”。

像这样的逻辑错误很难被发现,当然,它会编译,因为调用带有返回值的方法并且只是“假装”它返回void是完全可以接受的。 Visual Studio不关心你是否这样做;你的软件仍然可以编译和运行(尽管在某些情况下我认为它会发出警告)。当然,这是一个很棒的功能。想象一个简单的“InsertToDatabase”方法,它将新行的ID作为整数返回 - 很容易看出在某些情况下我们需要该ID,有些情况下我们可以不用它。

在这个软件的情况下,绝对没有任何理由在方法链的末尾避开“保存”功能。这是一个非常专业的实用工具,唯一的收获来自最后一步。

如果他们调用“With()”而不是“Save()”,我希望某人的软件在编译器级别失败

传统方式似乎是一项不可能完成的任务 - 但这就是我来找你们的原因。是否有一个属性我可以用来防止方法被“强制转换为无效”或某些?

注意:已经向我建议实现此目标的另一种方法是编写一套单元测试来强制执行此规则,并使用http://www.testdriven.net之类的东西绑定它们到编译器。这是一个可以接受的解决方案,但我希望有更优雅的东西。

7 个答案:

答案 0 :(得分:9)

我不知道在编译器级别强制执行此操作的方法。它经常被要求用于实现IDisposable的对象,但实际上并不是可执行的。

然而,一个可能有用的选择是设置你的类仅在DEBUG中,以获得一个记录/抛出/等的终结器。如果从未调用过Save()。这可以帮助您在调试时发现这些运行时问题,而不是依赖于搜索代码等。

但是,请确保在发布模式下不使用此功能,因为它会导致性能开销,因为添加不必要的终结器对GC性能非常不利。

答案 1 :(得分:4)

你可能需要特定的方法来使用这样的回调:

new FluentClass().Init<int>(x =>
{
    x.Save(y =>
    {
         y.With(-1),
         y.With(300)
    });
});

with方法返回一些特定的对象,获取该对象的唯一方法是调用x.Save(),它本身有一个回调函数,允许你设置不确定数量的with语句。所以init采用了这样的东西:

public T Init<T>(Func<MyInitInputType, MySaveResultType> initSetup) 

答案 2 :(得分:1)

如果您这样做Init并且With不返回FluentClass类型的对象,该怎么办?让他们返回,例如,UninitializedFluentClass包裹FluentClass对象。然后在.Save(0对象上调用UnitializedFluentClass在包装的FluentClass对象上调用它并返回它。如果他们不致电Save,他们就不会获得FluentClass个对象。

答案 3 :(得分:1)

我可以想到三个几个解决方案,不理想。

  1. AIUI你想要的是一个在临时变量超出范围时被调用的函数(例如,当它可用于垃圾收集时,但可能在一段时间内不会被垃圾收集)。 (参见:The difference between a destructor and a finalizer?)这个假设的函数会说“如果你在这个对象中构造了一个查询但没有调用save,则会产生错误”。 C ++ / CLI调用此RAII,并且在C ++ / CLI中,当对象不再使用时,存在“析构函数”的概念,并且当它最终被垃圾收集时调用“终结器”。非常令人困惑的是,C#只有一个所谓的析构函数,但这只是只有被垃圾收集器调用(它对于框架更早调用它是有效的,就像它立即部分清理对象一样,但AFAIK它没有做那样的事情)。所以你想要的是一个C ++ / CLI析构函数。不幸的是,AIUI映射到IDisposable的概念,它暴露了一个dispose()方法,可以在调用C ++ / CLI析构函数时调用,或者在调用C#析构函数时调用 - 但是AIUI你仍然需要调用“dispose” “手动,这打破了这一点?

  2. 稍微重构界面以更准确地传达概念。调用init函数,例如“prepareQuery”或“AAA”或“initRememberToCallSaveOrThisWontDoAnything”。 (最后一个是夸张,但可能有必要提出这一点。)

  3. 这更像是一个社会问题,而不是一个技术问题。界面应该可以很容易地做正确的事情,但程序员必须知道如何使用代码!让所有程序员在一起。简单解释一下这个简单的事实。如果有必要,让他们都签署一张纸,说他们理解,如果他们故意继续编写不做任何代码的代码,那么他们对公司来说就会变得无用而且会被解雇。

  4. 摆弄操作员链接的方式,例如。让每个intermediateClass函数组装一个包含所有参数的聚合中间类对象(你大多数时候这已经是(?)),但需要原始类的类似init的函数将其作为参数,而不是让它们在它之后链接,然后你可以保存,其他函数返回两个不同的类类型(基本上具有相同的内容),并且init只接受一个正确类型的类。

  5. 仍然问题的事实表明,你的同事需要一个有用的提醒,或者它们相当低,或者界面不是很清楚(也许它非常好,但是作者并没有意识到,如果你只是通过它而不是去了解它就不清楚,或者你自己误解了这种情况。技术解决方案会很好,但您应该考虑问题发生的原因以及如何更清楚地沟通,可能会询问高级人员的意见。

答案 4 :(得分:1)

经过深思熟虑和反复试验后,事实证明从Finalize()方法中抛出一个异常对我来说不起作用。显然,你根本做不到;异常被吃掉,因为垃圾收集操作非确定性。我无法让软件从析构函数中自动调用Dispose()。杰克五世的评论很好地解释了这一点;这是他发布的链接,用于冗余/重点:

The difference between a destructor and a finalizer?

更改语法以使用回调是一种聪明的方法,使行为万无一失,但商定的语法是固定的,我不得不使用它。我们的公司都是流利的方法链。说实话,我也是“out参数”解决方案的粉丝,但同样,底线是方法签名根本无法改变。

有关我的特定问题的有用信息包括我的软件仅 作为一系列单元测试的一部分运行 - 因此效率不是问题

我最终做的是使用 Mono.Cecil 来反映调用程序集(调用我的软件的代码)。请注意, System.Reflection 不足以满足我的目的,因为它无法查明方法引用,但我仍然需要(?)使用它来获取“调用程序集”本身(Mono.Cecil仍未记录,所以我可能需要更熟悉它才能完全取消System.Reflection;这还有待观察......)

我将 Mono.Cecil 代码放在 Init()方法中,结构现在看起来像:

public IntermediateClass<T> Init<T>()
{
    ValidateUsage(Assembly.GetCallingAssembly());
    return new IntermediateClass<T>();
}

void ValidateUsage(Assembly assembly)
{
    // 1) Use Mono.Cecil to inspect the codebase inside the assembly
    var assemblyLocation = assembly.CodeBase.Replace("file:///", "");
    var monoCecilAssembly = AssemblyFactory.GetAssembly(assemblyLocation);

    // 2) Retrieve the list of Instructions in the calling method
    var methods = monoCecilAssembly.Modules...Types...Methods...Instructions
    // (It's a little more complicated than that...
    //  if anybody would like more specific information on how I got this,
    //  let me know... I just didn't want to clutter up this post)

    // 3) Those instructions refer to OpCodes and Operands....
    //    Defining "invalid method" as a method that calls "Init" but not "Save"
    var methodCallingInit = method.Body.Instructions.Any
        (instruction => instruction.OpCode.Name.Equals("callvirt")
                     && instruction.Operand is IMethodReference
                     && instruction.Operand.ToString.Equals(INITMETHODSIGNATURE);

    var methodNotCallingSave = !method.Body.Instructions.Any
        (instruction => instruction.OpCode.Name.Equals("callvirt")
                     && instruction.Operand is IMethodReference
                     && instruction.Operand.ToString.Equals(SAVEMETHODSIGNATURE);

    var methodInvalid = methodCallingInit && methodNotCallingSave;

    // Note: this is partially pseudocode;
    // It doesn't 100% faithfully represent either Mono.Cecil's syntax or my own
    // There are actually a lot of annoying casts involved, omitted for sanity

    // 4) Obviously, if the method is invalid, throw
    if (methodInvalid)
    {
        throw new Exception(String.Format("Bad developer! BAD! {0}", method.Name));
    }
}

相信我,实际的代码看起来比我的伪代码更加丑陋....: - )

但Mono.Cecil可能是我最喜欢的玩具。

我现在有一个方法拒绝运行它的主体,除非调用代码“promises”之后也调用第二个方法。这就像一种奇怪的代码合同。我实际上正在考虑使这个通用和可重用。你们中的任何人都可以使用这种东西吗?说,如果它是一个属性?

答案 5 :(得分:0)

在实现IDisposable旁边的调试模式下,你可以设置一个定时器,如果没有调用resultmethod,它将在1秒后抛出异常。

答案 6 :(得分:0)

使用out参数!必须使用所有out

编辑:我不确定它会有所帮助,所以...... 它会破坏流利的语法。