C# 9 проверка записей

С новым типом записи 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));
    }
}

6 ответов

Я опаздываю на вечеринку, но это все еще может кому-то помочь...

На самом деле есть простое решение (но перед его использованием прочтите предупреждение ниже ). Определите базовый тип записи следующим образом:

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

    protected virtual void 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));
    }
}

Как видите, это почти точно код OP. Это просто, и это работает.

Однако будьте очень осторожны, если вы используете это: оно будет работать только со свойствами, определенными с синтаксисом «позиционной записи» (он же «первичный конструктор»).

Причина этого в том, что я делаю здесь что-то «плохое»: я вызываю виртуальный метод из конструктора базового типа. Обычно это не рекомендуется, поскольку конструктор базового типа запускается до конструктора производного типа, поэтому производный тип может быть не полностью инициализирован, поэтому переопределенный метод может работать неправильно.

Но для позиционных записей все происходит не в таком порядке: сначала инициализируются позиционные свойства, затем вызывается конструктор базового типа. Поэтому, когда метод вызывается, свойства уже инициализированы, поэтому он работает так, как ожидалось.

Если бы вы изменили record иметь явный конструктор (или свойства только для инициализации и без конструктора), вызов произойдет до того, как свойства будут установлены, поэтому он завершится ошибкой.

РЕДАКТИРОВАТЬ: еще одно досадное ограничение этого подхода заключается в том, что он не будет работать с (например ). Здесь используется другой (сгенерированный) конструктор, который не вызывает ...

Вы можете проверить свойство во время инициализации:

      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));
}

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

Следующее также достигает этого и намного короче (и imo также яснее):

      record Person (string FirstName, string LastName, int Age, Guid Id)
{
    private bool dummy1 = Check.StringArg(FirstName);
    private bool dummy2 = Check.StringArg(LastName);
    private bool dummy3 = 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;
        }
    }
}

Если бы только был способ избавиться от фиктивных объявлений.

Если вы можете жить без позиционного конструктора, вы можете выполнить проверку в 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;
        }
    }
}

В противном случае вам нужно будет создать собственный конструктор, как указано в комментарии выше.

(Кроме того, рассмотрите возможность использованияArgumentNullExceptionа такжеArgumentOutOfRangeExceptionвместо . Они наследуют от ArgumentException, но более конкретно о типе возникшей ошибки.)

(Источник)

Однако, насколько я могу судить, ни один из других ответов здесь не распространяется на оператора. Мне нужно убедиться, что мы не можем войти в недопустимое состояние в нашей модели предметной области, и нам нравится использовать записи там, где это применимо. Я наткнулся на этот вопрос, когда искал лучшее решение для этого. Я создал кучу тестов для разных сценариев и решений, однако большинство из них 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;
    }
}

К сожалению, в настоящее время вам нужны оба способа обновления/создания записи.

      record Person([Required] Guid Id, [Required] string FirstName, [Required] string LastName, int Age);
Другие вопросы по тегам