如何在.NET Core中正确使用代码合约

37
我想知道如何在.NET Core中正确使用Code Contracts。迄今为止,我尝试将CC添加到我的项目中,进行编译和调试。但每次使用Contract.Requires的调用都会出现混淆的信息和提示。该信息指出:

一个程序集必须使用代码合同二进制重写器(CCRewrite)进行重写,因为它正在调用Contract.Requires<TException>并且定义了CONTRACTS_FULL符号。从您的项目中删除任何显式定义的CONTRACTS_FULL符号并重新构建. CCRewrite....

据我所见,在项目属性中没有CC选项,并且我发现CC的Github存储库几乎已经死亡。是否有任何方法可以在.NET Core中成功使用CC呢?
如果不行,是否有任何简单的方法来替换它们?我使用Contract.RequiresContractClassAttribute。 替换Contract.Requires是很明显的,但ContractClassAttribute让我感到困惑 :-)

可能是重复问题:如何使用代码约束进行构建?,另外还有这个问题:https://dev59.com/13M_5IYBdhLWcg3w1G2N?noredirect=1&lq=1 - hatchet - done with SOverflow
1
不好意思,第一个只涉及已经实现的Contract.Requires,而第二个则涉及不同版本的.NET相似的问题。.NET Core的方法显然是不同的,所以这不是重复的 :-( 在VisualStudio中用于CC的UI在.NET Core项目中完全不存在。 - Jiří Zajíček
6
您已经发现Code Contracts不支持.NET Core是一个未解决的问题,至少目前为止没有被关注。那个用户jirizaj就是您,对吗?这很不幸,但我不确定您在这里希望得到什么。 - user743382
@hvd 是的,就是我。我没有想到,也不确定,所以我希望有人能找到解决方案。也许可以在某种程度上关闭CONTRACTS_FULL符号。也许有人会知道如何引入我最缺少的ContractClassAttribute。 - Jiří Zajíček
我认为没有简单的替代方法。你可以移除属性,将伙伴类改成接口上的扩展方法来进行前置条件检查,但是这样你必须在实现类的方法和属性中添加一行代码。虽然不是理想的解决方案,但至少前置条件代码只存在于一个地方。我认为要想得到比这更好的解决方案,就需要离开“简单”的领域了。如果我的评论不清楚,我可以发一个示例作为答案,但是它可能会被踩,因为它并没有真正回答你的问题。 - hatchet - done with SOverflow
显示剩余3条评论
2个回答

9
答案是:Code Contracts不再支持.NET Core,它可用于.NET Core 2.0,但不再维护。 请参阅文档页面上的官方声明:

注意

Code contracts在.NET 5+(包括.NET Core版本)中不受支持。请考虑使用可空引用类型替代。

还可以在Github线程 (Are Code Contracts going to be supported in .NET Core going forwards? #6361) 中找到相关信息:

我们停止了对代码合同的投资很久了。将其作为兼容性努力的一部分添加到.NET Core 2.0中,但我不建议新编写代码使用它。

FWIW,我们已经查看了许多使用这些功能的代码库,似乎绝大多数用途都是处理null值。这个问题的替代方案正在进行中,将使用C# 8的可空引用类型。

—— Immo Landwerth,Microsoft .NET团队的项目经理。https://twitter.com/terrajobst


4
首先,让我们了解一下什么是CodeContracts,根据microsoft docs的说法:

代码合同提供了一种在代码中指定前置条件、后置条件和对象不变量的方法。前置条件是进入方法或属性时必须满足的要求。后置条件描述了方法或属性代码退出时的期望结果。对象不变量描述了处于良好状态的类的预期状态。

也就是说,简单来说,CodeContracts 帮助我们简化代码中的测试。 我们如何使用代码合同? 考虑以下示例:
if ( x == null ) throw new ...  
Contract.EndContractBlock(); // All previous "if" checks are preconditions  

“preconditions” 的意思是两种情况之一:

if-then-throw语句以这种形式出现时,工具会将其识别为传统的requires语句。如果没有其他合同跟随if-then-throw序列,请使用Contract.EndContractBlock方法结束代码。
您可以在“后置条件”中使用它:
“后置条件”是指方法终止时的状态约定。在退出方法之前,将检查后置条件。运行时分析器确定了失败后置条件的运行时行为。
与先决条件不同,后置条件可能引用可见性较低的成员。客户端可能无法理解或利用使用私有状态表达的某些信息的后置条件,但这不会影响客户端正确使用该方法的能力。
换句话说,后置条件帮助我们测试我们的方法。
例如:
Contract.Ensures( this.F > 0 );

请注意特殊的后置条件:
  • 您可以使用表达式Contract.Result<T>()引用方法返回值的后置条件,其中T被替换为方法的返回类型。当编译器无法推断类型时,您必须显式提供它。
  • 后置条件中的预状态值指方法或属性开始时表达式的值。它使用表达式Contract.OldValue<T>(e),其中Te的类型。每当编译器能够推断其类型时,您可以省略通用类型参数。(例如,C#编译器总是推断类型,因为它需要一个参数。)关于e中可能发生的内容以及旧表达式可能出现的上下文有一些限制。一个旧表达式不能包含另一个旧表达式。最重要的是,旧表达式必须引用在方法的前提状态下存在的值。换句话说,它必须是一个只要方法的前提条件为真就可以评估的表达式。

最后,您有不变量:

对象不变式是指每个类实例在可见于客户端时应为真的条件。它们表达了对象被认为是正确的条件。

也就是说,不变量有助于测试我们的类代码和实例。

例如:

[ContractInvariantMethod]  
protected void ObjectInvariant ()   
{  
Contract.Invariant(this.y >= 0);  
Contract.Invariant(this.x > this.y);  
...  
}  

CodeContracts 正确使用的完整示例:

using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Net.Http.Headers;
using System.Diagnostics.Contracts;

namespace System.Net.Http
{
    public class FormUrlEncodedContent : ByteArrayContent
    {
        public FormUrlEncodedContent(IEnumerable<KeyValuePair<string, string>> nameValueCollection)
            : base(GetContentByteArray(nameValueCollection))
        {
            Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
        }

        private static byte[] GetContentByteArray(IEnumerable<KeyValuePair<string, string>> nameValueCollection)
        {
            if (nameValueCollection == null)
            {
                throw new ArgumentNullException(nameof(nameValueCollection));
            }
            Contract.EndContractBlock();

            // Encode and concatenate data
            StringBuilder builder = new StringBuilder();
            foreach (KeyValuePair<string, string> pair in nameValueCollection)
            {
                if (builder.Length > 0)
                {
                    builder.Append('&');
                }

                builder.Append(Encode(pair.Key));
                builder.Append('=');
                builder.Append(Encode(pair.Value));
            }

            return HttpRuleParser.DefaultHttpEncoding.GetBytes(builder.ToString());
        }

        private static string Encode(string data)
        {
            if (String.IsNullOrEmpty(data))
            {
                return String.Empty;
            }
            // Escape spaces as '+'.
            return Uri.EscapeDataString(data).Replace("%20", "+");
        }

        internal override Stream TryCreateContentReadStream() =>
            GetType() == typeof(FormUrlEncodedContent) ? CreateMemoryStreamForByteArray() : // type check ensures we use possible derived type's CreateContentReadStreamAsync override
            null;
    }
}

12
这是有关代码契约及其使用的良好解释,但它并没有回答提问者的问题。@Kousic的回答更好。 - Hintham
有没有办法在.NET Core中成功使用CC?根据这个,我回答了这个问题 :) - Barr J
1
是的,你解释了它在Core中的使用方式,谢谢 :-)。我认为这种方式不值得在项目中再引用一次。CC最好的部分是能够扩展接口并限制输入到实现中。有一个定义了合同的合同类有助于增加代码可读性(与接口相同文件中的一个内部类)和安全性(你有多少次忘记或初级人员忽略参数检查 - 我考虑简单和明显的检查,如空检查)。这使我能够立即在所有实现中使用合同。 - Jiří Zajíček
3
我下投票反对这个答案,因为虽然在删除 FormUrlEncodedContentinternal 部分后,它可以在 C# 8.0 中编译通过,但代码合约的实际部分对构建过程没有影响(即将 null 传递给 GetContentByteArray 不会导致在构建时出现错误或警告(当禁用 C# 8.0 可空引用类型时)。 - Dai
不,它描述了如何在Framework中使用代码合约,而不是在Core中使用,因为在Core中已不再支持。 - digitig

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接