C# 9 – Метод init для доступа к свойству класса

C# logoДо C# 9 в языке поддерживалось только два типа методов доступа к свойствам класса: get и set. Теперь появился еще один – init. Он позволяет создавать удобные конструкции для инициализации экземпляра класса, при этом делая свойства неизменяемыми. Разберемся подробнее.

Минимальная платформа с полной поддержкой: .NET 5
Возможность использования на предыдущих платформах: при добавлении специального класса.

Назначение

До C# 9 сделать свойство объекта неизменяемым можно было следующим способом: объявить его только с методом доступа get и присвоить ему значение в конструкторе. Однако, в данном подходе есть следующие недостатки:

  1. Много однотипного кода передачи значения из параметра конструктора в свойство.
  2. Конструкторы может получиться с большим числом параметров, даже если не все они обязательные.

В C# 9 появился новый метод доступа до свойства – init, который разрешает присвоение значения только:

  • в конструкторе класса или его наследника;
  • в инициализаторе объекта класса или его наследника;
  • в выражении with для записей (record);
  • как именованный параметр атрибута.

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

Стоит отметить, что init и set – взаимоисключающие методы доступа. Использоваться с конкретным свойством может только один из них.

Синтаксис

Используется init аналогично другим методам доступа к свойству. Рассмотрим синтаксис на примере:

public class User
{
    public int Id { get; init; }
    public string Name { get; init; } = "No name";
}
…
var user1 = new User(); //  Id = default(int), Name = "No name"
var user2 = new User() { Id = 1 }; // Id = 1, Name = "No name"
var user3 = new User() { Id = 1, Name = "John Doe" };

В данном случае даже не потребовалось явно объявлять конструктор. Объявление типа получилось очень лаконичным.

В примере были использованы автоматически реализованные свойства. Но при необходимости, разработчик может предоставить собственную реализацию (как в случае с остальными методами доступа).

Метод init отлично подходит для свойств которые должны быть неизменяемы, но при этом их инициализация опциональна.

Начиная с C# 9, можно определять очень удобные в использовании контракты создания класса, когда обязательные параметры передаются через параметры конструктора, а необязательные – через свойства с методом доступа init.

Например:

public class DataConverter : IDataConverter
{
    public DataConverterSetting Settings { get; init; }
    …
    public DataConverter(IDataSource source) // Обязательный параметр
    {
        // Устанавливаем настройки по умолчанию, 
        // которые могут быть переопределены в инициализаторе
        this.DataConverterSetting = new DefaultDataConverterSetting();
        …
    }
}
…
var converter1 = new DataConverter(dataSource); // используем настройки по умолчанию
var converter2 = new DataConverter(dataSource); // устанавливаем произвольные настройки 
{
    DataConverterSetting = new CustomDataConverterSetting()
}

Теперь пользователь класса JsonConverter может или указать свои настройки в инициализаторе экземпляра или оставить их по умолчанию.

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

Особенности использования

Поведение при наследовании

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

public class Pair
{
    public int Id { get; }
    public string Value { get; init; } = "Default value";

    public Pair(int id) => Id = id;
}

public class DataRecord : Pair
{
    public DataRecord(int id)
        : base(id)
    {
        this.Value = "DataRecord default value";
    }
}
…
var record1 = new DataRecord(id: 1);
var record2 = new DataRecord(id: 2)
{
    Value = "Custom value"
};

Изменение readonly полей

Так как метод init может быть вызван только при инициализации объекта, то ему разрешено изменять значение readonly полей класса.

public class DataConverter : IDataConverter
{
    private readonly DataConverterSetting _settings;

    public DataConverterSetting Settings { 
        get => _settings; 
        init => _settings = value; 
    }
    …
}

Использование с ссылочными типами, допускающими значение null

Присвоение значений свойствам с методом init не является обязательной. Поэтому при использовании ссылочных типов, допускающих значение null, компилятор будет выдавать соответствующее предупреждение или ошибку (в зависимости от настроек) для ссылочных типов не допускающих значение null при отсутствии значения по умолчанию, установленного в конструкторе.

Поддержка в .NET 4.x, .NET Standard и .NET Core

Для компилятора для работы с методом доступа init требуется следующий класс:

namespace System.Runtime.CompilerServices
{
    public sealed class IsExternalInit { }
}

Он уже определен в ядре .NET 5. А вот для предыдущих платформ его придется добавить в проект самостоятельно. Документация явно указывает что данный класс не обязан находится в коде corelib. А его поиск идет исключительно по полному имени в следующем порядке:

  • в текущем проекте.
  • в ядре платформы (corelib).

Таким образом, его самостоятельное добавление в проекты для .NET 4.x, .NET Standard и .NET Core вполне легально.

Комментарии (2) -

Сергей 18.11.2020 23:21:13

Спасибо, Андрей, за обзор

Пожалуйста.

Добавить комментарий