C# 9 记录验证

Sim*_*tes 20 c# validation c#-9.0 c#-record-type

使用 C# 9 的新记录类型,如何在对象的构造过程中注入自定义参数验证/空检查/等而无需重新编写整个构造函数

类似的东西:

record Person(Guid Id, string FirstName, string LastName, int Age)
{
    override void Validate()
    {
        if(FirstName == null)
            throw new ArgumentException("Argument cannot be null.", nameof(FirstName));
        if(LastName == null)
            throw new ArgumentException("Argument cannot be null.", nameof(LastName));
        if(Age < 0)
            throw new ArgumentException("Argument cannot be negative.", nameof(Age));
    }
}
Run Code Online (Sandbox Code Playgroud)

Tho*_*que 21

我参加聚会迟到了,但这可能仍然对某人有帮助......

实际上有一个简单的解决方案(但请在使用之前阅读下面的警告)。像这样定义基本记录类型:

public abstract record RecordWithValidation
{
    protected RecordWithValidation()
    {
        Validate();
    }

    protected virtual void Validate()
    {
    }
}
Run Code Online (Sandbox Code Playgroud)

并使您的实际记录继承RecordWithValidation并覆盖Validate

record Person(Guid Id, string FirstName, string LastName, int Age) : RecordWithValidation
{
    protected override void Validate()
    {
        if (FirstName == null)
            throw new ArgumentException("Argument cannot be null.", nameof(FirstName));
        if (LastName == null)
            throw new ArgumentException("Argument cannot be null.", nameof(LastName));
        if (Age < 0)
            throw new ArgumentException("Argument cannot be negative.", nameof(Age));
    }
}
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,这几乎就是OP的代码。这很简单,而且很有效。

但是,如果您使用它,请务必小心:它仅适用于使用“位置记录”语法(也称为“主构造函数”)定义的属性。

原因是我在这里做了一些“坏”的事情:我从基本类型的构造函数调用虚拟方法。通常不鼓励这样做,因为基类型的构造函数在派生类型的构造函数之前运行,因此派生类型可能未完全初始化,因此重写的方法可能无法正常工作。

但对于位置记录,事情不会按这个顺序发生:首先初始化位置属性,然后调用基类型的构造函数。因此,当Validate调用该方法时,属性已经初始化,因此它可以按预期工作。

如果您要将Person记录更改为具有显式构造函数(或仅初始化属性而无构造函数),则调用Validate将在设置属性之前发生,因此会失败。

编辑:这种方法的另一个恼人的限制是它不能与with(例如person with { Age = 42 })一起使用。这使用了不同的(生成的)构造函数,它不会调用Validate...

  • @ThomasLevesque 抱歉,这是一个旧线程,但以防万一它对其他偶然发现此问题的人有用。如果将复制构造函数添加到 RecordWithValidation 并调用其中的 Validate 方法,则将遵循该基类,并且在使用“with”时调用 Validate 方法。例如,添加以下构造函数: public RecordWithValidation(RecordWithValidation other) { Validate(); 我通过查看低级生成的 C# 并复制记录的自动生成的复制构造函数的签名发现了这一点。 (3认同)
  • 不过,这与上面使用 init 和初始化器的问题具有相同的问题,因为它在使用“with”语法创建新实例时不进行验证。但是......无论如何,如果不自己编写完整的属性,就无法做到这一点。 (2认同)

Aik*_*Aik 16

您可以在初始化期间验证该属性:

record Person(Guid Id, string FirstName, string LastName, int Age)
{
    public string FirstName {get;} = FirstName ?? throw new ArgumentException("Argument cannot be null.", nameof(FirstName));
    public string LastName{get;} = LastName ?? throw new ArgumentException("Argument cannot be null.", nameof(LastName));
    public int Age{get;} = Age >= 0 ? Age : throw new ArgumentException("Argument cannot be negative.", nameof(Age));
}
Run Code Online (Sandbox Code Playgroud)

https://sharplab.io/#gist:5bfbe07fd5382dc2fb38ad7f407a3836

  • 不幸的是,这仍然需要复制记录类型的成员名称和类型(作为参数和属性),这有点违背了首先使用“记录”类型的要点之一:简洁性。 (6认同)
  • 这不是正确的答案,因为使用新的“with”表达式时不会进行验证。请参阅以下测试:https://gist.github.com/C0DK/1df531dc5d8bbde6faffe2dc887cd4ef (6认同)
  • @Dai,对于类,您可以在构造函数中编写更多行来设置所有属性。然而,使用这种方法,您只需要为要检查 null 的属性编写额外的行。_顺便说一句,您可以为其创建一个片段。_ (2认同)
  • @CasperBang嗯,没有办法如何覆盖`with`表达式,因为它发出简单的赋值。这就是为什么该示例没有为属性定义 setter,并且它有效地禁用仅 getter 属性的“with”语句。 (2认同)

Bat*_*tox 7

下面的代码也实现了它,并且更短(而且我认为也更清晰):

record Person (string FirstName, string LastName, int Age, Guid Id)
{
    private bool _dummy = Check.StringArg(FirstName)
        && Check.StringArg(LastName) && Check.IntArg(Age);

    internal static class Check
    {
        static internal bool StringArg(string s) {
            if (s == "" || s == null) 
                throw new ArgumentException("Argument cannot be null or empty");
            else return true;
        }

        static internal bool IntArg(int a) {
            if (a < 0)
                throw new ArgumentException("Argument cannot be negative");
            else return true;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果有一种方法可以消除虚拟变量就好了。

  • 请参阅 String.IsNullOrEmpty (9认同)
  • @AvrohomYisroel 继承不应该被滥用作为混入的穷人的替代品。相反,请使用“using static”导入。 (3认同)
  • 您还应该将参数名称传递给异常。对于 C# 10,请考虑使用 [CallerArgumentExpressionAttribute](https://learn.microsoft.com/en-gb/dotnet/api/system.runtime.compilerservices.callerargumentExpressionattribute?view=net-5.0)。 (2认同)

Chr*_*ton 6

如果您可以在没有位置构造函数的情况下生活,您可以在init每个属性的需要它的部分完成验证:

record Person
{
    private readonly string _firstName;
    private readonly string _lastName;
    private readonly int _age;
    
    public Guid Id { get; init; }
    
    public string FirstName
    {
        get => _firstName;
        init => _firstName = (value ?? throw new ArgumentException("Argument cannot be null.", nameof(value)));
    }
    
    public string LastName
    {
        get => _lastName;
        init => _lastName = (value ?? throw new ArgumentException("Argument cannot be null.", nameof(value)));
    }
    
    public int Age
    {
        get => _age;
        init =>
        {
            if (value < 0)
            {
                throw new ArgumentException("Argument cannot be negative.", nameof(value));
            }
            _age = value;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

否则,您将需要创建上面评论中提到的自定义构造函数。

(顺便说一句,请考虑使用ArgumentNullExceptionandArgumentOutOfRangeException而不是ArgumentException。这些继承自ArgumentException,但对于发生的错误类型更具体。)

(来源)

  • *“如果你可以没有位置构造函数”* ...以及解构函数。 (2认同)
  • 这不会取代构造函数:如果未设置,构造对象中的“FirstName”可能为 null,而构造函数排除了这种可能性。有没有办法强制记录运行验证器(逻辑上)(例如,通过显式地将“default”分配给调用代码未分配的任何属性)? (2认同)

CDK*_*CDK 5

这里的其他答案都非常好,但是with据我所知,都没有涵盖操作员。我需要确保我们不能在域模型上输入无效状态,并且我们喜欢在适用的情况下使用记录。我在寻找最佳解决方案时偶然发现了这个问题。我已经为不同的场景和解决方案创建了一堆测试,但是大多数都因with表达式而失败。我尝试过的变体可以在这里找到: https: //gist.github.com/C0DK/d9e8b99deca92a3a07b3a82ba4a6c4f8

我最终采用的解决方案是:

public record Foo(int Value)
{
    private readonly int _value = GetValidatedValue(Value);

    public int Value
    {
        get => _value;
        init => _value = GetValidatedValue(value);
    }

    private static int GetValidatedValue(int value)
    {
        if (value < 0) throw new Exception();
        return value;
    }
}
Run Code Online (Sandbox Code Playgroud)

遗憾的是,您目前需要两者来处理更新/创建记录的两种方式。

  • 哇,这与使用“struct”类型一样痛苦(其中“this == default(T)”随时可能发生,因此所有不可为空的引用类型字段仍然可以为“null”),除了我们有人告诉“记录”类型意味着未来...... aieeee (2认同)
  • @Dai 不,据我所知记录还很早,他们正在下一版本中的 init 运算符上工作。记录似乎执行不力,这令人遗憾。这个主意超级棒。我建议尽可能改用 F#。在那里,您将获得开箱即用的大部分出色功能:)(我也很遗憾我的答案没有进一步提高,因为据我所知,它“更正确”) (2认同)
  • 是的,每一个令人兴奋的新 C# 功能最终都会让人大失所望。要让某些东西用这种语言工作需要很多技巧。 (2认同)