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

Bok*_*non 10 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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

new FluentClass().Init<int>().With(-1).With(300).With(42).Save();
Run Code Online (Sandbox Code Playgroud)

问题是人们正在以这种方式使用它(认为它实现与上面相同):

new FluentClass().Init<int>().With(-1).With(300).With(42);
Run Code Online (Sandbox Code Playgroud)

因此,普遍存在的问题是,在完全出于好意的情况下,另一位开发人员实际上改变了"Init"方法的名称,以表明该方法正在进行软件的"实际工作".

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

在这个软件的情况下,绝对没有任何理由在方法链的末尾避开"保存"功能.它是一个非常专业的实用程序,唯一的收益来自最后一步.

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

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

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

Ree*_*sey 9

我不知道在编译器级别强制执行此操作的方法.它经常被要求实现的对象IDisposable,但不是真正可执行的.

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

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


Bok*_*non 1

经过深思熟虑和反复试验,结果发现从 Finalize() 方法抛出异常对我来说不起作用。显然,你根本无法做到这一点;异常会被耗尽,因为垃圾收集的运行是不确定的。我也无法让软件从析构函数自动调用 Dispose() 。Jack V. 的评论很好地解释了这一点;这是他发布的链接,用于冗余/强调:

析构函数和终结器之间的区别?

更改语法以使用回调是一种使行为万无一失的聪明方法,但商定的语法是固定的,我必须使用它。我们公司致力于流畅的方法链。说实话,我也是“输出参数”解决方案的粉丝,但同样,底线是方法签名根本无法更改。

关于我的特定问题的有用信息包括这样一个事实:我的软件作为一组单元测试的一部分运行 - 因此效率不是问题。

我最终做的是使用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));
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

我现在有一个方法,它拒绝运行其主体,除非调用代码“承诺”之后也调用第二个方法。这就像一种奇怪的代码契约。我实际上正在考虑使其通用且可重用。你们中有人会用到这样的东西吗?比如说,如果它是一个属性呢?