c#在设计时检查值

c# check value at design time

在 Visual Studio 2022 年的设计期间,是否有任何方法(可能是使用 Contracts)来检查变量的值是否符合某些规则?

int 在 C# 中被大量使用,但通常我们真正想要的是 uint,也就是说只有正值,标准是检查是否传递负值以抛出 error/exception/etc.

但是是否有某种方法可以编写方法并告诉 Visual Studio 或编译器或任何“此参数必须 >= 0”,并且每当开发人员传递小于 0 的值时出错显示在 Visual Studio 错误列表中?

通常 Code Contracts 将是完美的 - 但似乎令人沮丧的是,该功能在微软从 .NET Framework 到 .NET Core 的大跃进中无人问津并被遗弃,这意味着需要一些替代方法。

虽然完全有可能使用 Roslyn 代码分析从根本上重新实现代码契约,但这并非易事,可能需要您 许多个月 来构建一个可以证明验证变量在其生命周期内可能的运行时边界(即 mathematically speaking, impossible to solve in the general case)。

所以另一种方法是使用 refinement type / predicate-type / dependent-type (我忘记了它们之间的确切区别) - 但总体要点是你使用了一种新的、不同的类型来表示对其包含的值的约束。由于 C# 是 statically-typed,这意味着可以使用类型来表示运行时状态不变量。

在 C# 中,这些类型通常实现为不可变 readonly struct 类型,因为(通常)开销为零。您还可以将其与运算符重载、IEquatable<T>、extension-methods、作用域 out 参数和 implicit 转换相结合,以获得非常符合人体工程学的 refinement-type 体验。

(这是我非常同情Java用户的地方,因为Java没有value-types,也没有运算符重载,也没有扩展方法,也没有user-defiend 隐式转换 - 哎哟).

注意:在定义 implicit 转换时,非常重要 只定义隐式转换 from (narrow) refined类型返回到更广泛的类型(因为那是 总是 会成功) - 你 绝不能 定义从更广泛类型到约束类型的隐式转换类型,因为如果更宽的值无效,那么当您的验证代码抱怨时将导致运行时异常,编译器将无法 pick-up on.


所以在你的情况下,你想要一个类型来表示一个正数,non-zero Int32 值 - 所以你想要这样的东西:

(此代码省略了实现 VS 喜欢抱怨的 struct/IEquatable<> 样板 - 但它包含在下面的 *.snippet 版本中)。

public static class PositiveInt32Extensions
{
    public static Boolean IsPositive( this Int32 candidate, out PositiveInt32 value ) => PositiveInt32.TryCreate( candidate, out value );
}

public readonly struct PositiveInt32
{
    public static Boolean TryCreate( Int32 candidate, out PositiveInt32  value )
    {
        if( candidate > 0 )
        {
            value = new PositiveInt32( candidate );
            return true;
        }
        else
        {
            value = default;
            return false;
        }
    }

    private readonly Int32 value;

    public PositiveInt32( Int32 value )
    {
        if( value < 1 ) throw new ArgumentOutOfRangeException( nameof(value), actualValue: value, message: "Value must be positive." );
        this.value = value;
    }

    public static implicit operator Int32( PositiveInt32 self ) => self.value;

    // NOTE: This implicit conversion will fail when `unsignedValue  > UInt32.MaxValue / 2`, but I assume that will never happen.
    public static implicit operator PositiveInt32 ( UInt32 unsignedValue ) => new PositiveInt32( (Int32)unsignedValue );
}

Here's my own personal refinement-type *.snippet for Visual Studio - 希望对你有用:


<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<!--

Usage/installation instructions:
1. Save to a file `refine.snippet` somewhere (e.g. in `C:\Users\You\Documents\Visual Studio {year}\Code Snippets\Visual C#\My Code Snippets`).
   * If saved outside your `Visual Studio {year}` folder, or if it isn't detected, add it manually via <kbd>Tools > Code Snippets Manager...</kbd> (Tip: ensure the top "Language" drop-down says "CSharp" as it defaults to ASP.NET for some reason).
2. To try it out, open a .cs file and move your cursor/caret to inside a `namespace`, then type the word "`refine`" and IntelliSense should list it as a snippet in the completion-list popup.
   * If it doesn't appear despite being recognized by Code Snippets Manager ensure VS is configured to list Snippets in the code completion list (Tools > Options > Text Editor > C# > IntelliSense > Snippets behavior > "Always include snippets").
3. Press <kbd>Tab</kbd> once or twice (it varies...) and it should be inserted, with the caret moved to the first `$refinementname$` placeholder. Type the new value then press <kbd>Tab</kbd> to move to the $supertypename$ placeholder. Press <kbd>Tab</kbd> or <kbd>Enter</kbd> when done.

-->

    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>refine</Title>
            <Shortcut>refine</Shortcut>
            <SnippetTypes>
                <!-- There are only 2 types of Snippets: "Expansion" and "Surround-With", btw: https://docs.microsoft.com/en-us/visualstudio/ide/code-snippets?view=vs-2022 -->
                <SnippetType>Expansion</SnippetType>
            </SnippetTypes>
            <Description>Refinment type represented by a public readonly struct with implicit conversion support.</Description>
        </Header>
        <Snippet>
            <Declarations>
                <Object Editable="true">
                    <ID>refinementname</ID>
                    <Type>Object</Type>
                    <ToolTip>PascalCased summary of the refinement type's predicate - this is concatenated with $supertype$ for the final struct name. e.g. &quot;ValidatedEmailAddress&quot; (&quot;ValidatedEmailAddressUser&quot;)</ToolTip>
                    <Default>NewRefinement</Default>
                </Object>
                <Object Editable="true">
                    <ID>supertype</ID>
                    <Type>Object</Type>
                    <ToolTip>The name of the type that is being refined. e.g. &quot;User&quot; (for &quot;ValidatedEmailAddressUser&quot;)</ToolTip>
                    <Default>SupertypeName</Default>
                </Object>
            </Declarations>
            <!-- Inside <Code>, the only reserved-token names are `$end$` and `$selected$`. Both can only be used at-most once. -->
            <!-- BTW, for this snippet specifically, should the `.Value` property's getter self-validating? or always trust the constructor instead? What's the best way to prevent `default(StructType)` thesedays? -->
            <Code Language="CSharp" Kind="type decl"><![CDATA[
    public static partial class RefinementExtensions
    {
        public static Boolean Is$refinementname$$supertype$( this $supertype$ value, [NotNullWhen(true)] out $refinementname$$supertype$? valid )
        {
            return $refinementname$$supertype$.TryCreate( value, out valid );
        }

        /// <summary>Throws <see cref="ArgumentException"/> if <paramref name="value"/> does not satisfy the refinement predicate.</summary>
        /// <exception cref="ArgumentException"></exception>
        public static $refinementname$$supertype$ To$refinementname$$supertype$( this $supertype$ value )
        {
            return $refinementname$$supertype$.Create( value );
        }
    }

    public readonly struct $refinementname$$supertype$ : IReadOnly$supertype$, IEquatable<$refinementname$$supertype$>, IEquatable<$supertype$>
    {
        #region Create / TryCreate

        /// <summary>Throws <see cref="ArgumentException"/> if <paramref name="value"/> does not satisfy the refinement predicate.</summary>
        /// <exception cref="ArgumentException"></exception>
        public static $refinementname$$supertype$ Create( $supertype$ value )
        {
            if( TryCreate( value, out $refinementname$$supertype$? valid ) ) return valid.Value;
            else throw new ArgumentException( paramName: nameof(value), message: "Argument object does not satisfy " + nameof($refinementname$$supertype$) + "'s refinement predicate." );
        }

        /// <summary>Returns <see langword="null"/> if <paramref name="value"/> does not satisfy the refinement predicate.</summary>
        public static $refinementname$$supertype$? TryCreate( $supertype$ value )
        {
            return TryCreate( value, out $refinementname$$supertype$? valid ) ? valid : null;
        }

        /// <summary>Returns <see langword="false"/> if <paramref name="value"/> does not satisfy the refinement predicate.</summary>
        public static Boolean TryCreate( $supertype$ value, [NotNullWhen(true)] out $refinementname$$supertype$? valid )
        {
            if( CONDITION )
            {
                valid = new $refinementname$$supertype$( value );
                return true;
            }

            return false;
        }

        #endregion

        public static implicit operator $supertype$( $refinementname$$supertype$ self )
        {
            return self.Value;
        }

        private $refinementname$$supertype$( $supertype$ value )
        {
            this.value_doNotReadThisFieldExceptViaProperty = value ?? throw new ArgumentNullException(nameof(value));
        }

        private readonly $supertype$ value_doNotReadThisFieldExceptViaProperty;

        public $supertype$ Value => this.value_doNotReadThisFieldExceptViaProperty ?? throw new InvalidOperationException( "This " + nameof($refinementname$$supertype$) + " value is invalid." );

        public override String ToString() => this.Value.ToString();

        #region IReadOnly$supertype$

        // TODO?

        #endregion

        #region IEquatable<$refinementname$$supertype$>, IEquatable<$supertype$>

        private Boolean IsDefault => this.value_doNotReadThisFieldExceptViaProperty is null;

        public override Boolean Equals( Object? obj )
        {
            if( this.IsDefault )
            {
                return obj is null;
            }
            else if( obj is $supertype$ super )
            {
                return this.Equals( super: super );
            }
            else if( obj is $refinementname$$supertype$ other )
            {
                return this.Equals( other: other );
            }
            else
            {
                return false;
            }
        }

        public Boolean Equals( $refinementname$$supertype$ other )
        {
            return ( this.IsDefault && other.IsDefault ) || ( this.Value == other.Value );
        }

        public Boolean Equals( $supertype$? super )
        {
            return !this.IsDefault && ( this.Value == super );
        }

        public override Int32 GetHashCode()
        {
            if( this.IsDefault ) return 0;

            return this.Value.GetHashCode(); // return HashCode.Combine( this.Value );
        }

        public static Boolean operator ==( $refinementname$$supertype$ left, $refinementname$$supertype$ right )
        {
            return left.Equals( other: right );
        }

        public static Boolean operator !=( $refinementname$$supertype$ left, $refinementname$$supertype$ right )
        {
            return !left.Equals( other: right );
        }

        #endregion

    }$end$]]>
            </Code>
            <Imports>
                <Import>
                    <Namespace>System</Namespace>
                </Import>
                <Import>
                    <Namespace>System.Diagnostics.CodeAnalysis</Namespace>
                </Import>
            </Imports>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>