с


Records — это новая функция в C # 9. Записи — это специальные классы, заимствованные из структур, поскольку они имеют равенство на основе значений. Вы можете рассматривать их как гибрид двух категорий типов. По умолчанию они более или менее неизменяемы и содержат синтаксический сахар, чтобы сделать объявление более легким и кратким. Однако синтаксический сахар может скрыть более стандартные задачи, такие как изменение поведения конструктора по умолчанию. В некоторых случаях вам, вероятно, потребуется сделать это для проверки. В этой статье показано, как этого добиться.

Возьмите этот простой пример класса:

public class StringValidator
{
    public string InputString { get; }

    public StringValidator(string inputString)
    {
        if (string.IsNullOrEmpty(inputString)) throw new ArgumentNullException(nameof(inputString));

        InputString = inputString;
    }
}

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

    public record StringValidator(string InputString);

Он понятен и краток, но не сразу понятно, как проверить строку. Это определение сообщает компилятору, что будет свойство с именем InputString, и конструктор передаст значение этому свойству из параметра. Нам нужно удалить синтаксический сахар, чтобы проверить строку. К счастью, это легко. Нам не нужно использовать новый синтаксис для определения наших записей. Мы можем определить запись аналогично классу, но изменить ключевое слово class на record.

public record StringValidator
{
    public string InputString { get;  }

    public StringValidator(string inputString)
    {
        if (string.IsNullOrEmpty(inputString)) throw new ArgumentNullException(nameof(inputString));

        InputString = inputString;
    }
}

К сожалению, это означает, что мы не можем использовать неразрушающую мутацию. Ключевое слово with дает нам возможность создать новую версию записи с измененными некоторыми свойствами. Это означает, что мы не изменяем исходный экземпляр записи, но получаем его копию. Это общий подход для программирования Fluent API и функционального стиля. Это позволяет нам сохранять неизменность.

Чтобы разрешить неразрушающую мутацию, нам нужно добавить аксессор свойства init. Он работает аналогично конструктору, но вызывается только во время инициализации объекта. Вот более полное решение, реализующее аксессор init. Это позволяет использовать общую логику конструктора и логику инициализации.

using System;

namespace ConsoleApp25
{
    class Program
    {
        static void Main(string[] args)
        {
            //This throws an exception from the constructor
            //var stringValidator = new StringValidator(null);

            var stringValidator1 = new StringValidator("First");
            var stringValidator2 = stringValidator1 with { InputString = "Second" };
            Console.WriteLine(stringValidator2.InputString);

            //This throws an exception from the init accessor
            //var stringValidator3 = stringValidator1 with { InputString = null };

            //Output: Second
        }
    }

    public record StringValidator
    {
        private string inputString;

        public string InputString
        {
            get => inputString;
            init
            {
                //This init accessor works like the set accessor
                ValidateInputString(value);
                inputString = value;
            }
        }

        public StringValidator(string inputString)
        {
            ValidateInputString(inputString);
            InputString = inputString;
        }

        public static void ValidateInputString(string inputString)
        {
            if (string.IsNullOrEmpty(inputString)) throw new ArgumentNullException(nameof(inputString));
        }
    }
}

Должны ли конструкторы записей иметь логику?

Это спорная дискуссия, выходящая за рамки данной статьи. Многие люди возразят, что не следует помещать логику в конструкторы. Дизайн записей побуждает вас не помещать логику в конструктор или средство доступа init. Вообще говоря, записи должны отражать состояние ваших данных во времени. Вам не нужно применять логику, потому что предполагается, что вы знаете состояние своих данных на данный момент. Однако, как и в случае с любой другой конструкцией программирования, невозможно узнать, какие варианты использования могут возникать из записей. Вот пример из библиотеки Urls, которая рассматривает URL-адреса как неизменяемые записи:

using System.Net;

namespace Urls
{
    public record QueryParameter
    {
        private string? fieldValue;

        public string FieldName { get; init; }
        public string? Value
        {
            get => fieldValue; init
            {
                fieldValue = WebUtility.UrlDecode(value);
            }
        }

        public QueryParameter(string fieldName, string? value)
        {
            FieldName = fieldName;
            fieldValue = WebUtility.UrlDecode(value);
        }

        public override string ToString()
            => $"{FieldName}{(Value != null ? "=" : "")}{WebUtility.UrlEncode(Value)}";
    }
}

Мы гарантируем, что декодируем значение запроса при его сохранении, а затем кодируем его, когда используем его как часть URL-адреса. Вы могли бы задать вопрос: почему бы не сделать все записи? Похоже, что с этим связаны подводные камни, но мы идем на дальше, и нам еще предстоит наметить лучшие практики для записей в контексте C #.

Подведение итогов

Разработчикам потребуется несколько лет, чтобы смириться с записями и заложить основные правила их использования. В настоящее время у вас есть чистый лист, и вы можете экспериментировать, пока «эксперты» не скажут вам обратное. Мой совет — использовать записи только для представления фиксированных данных и минимальной логики. По возможности используйте синтаксический сахар. Однако есть очевидные сценарии, в которых минимальная проверка в конструкторе может быть практичной. Используйте свое суждение, обсудите со своей командой и взвесьте все за и против.