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. "ValidatedEmailAddress" ("ValidatedEmailAddressUser")</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. "User" (for "ValidatedEmailAddressUser")</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>
在 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. "ValidatedEmailAddress" ("ValidatedEmailAddressUser")</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. "User" (for "ValidatedEmailAddressUser")</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>