Как вызвать один конструктор из другого в C# для избежания дублирования кода?
У меня есть класс C# с двумя конструкторами, которые инициализируют поля только для чтения:
public class Sample
{
public Sample(string theIntAsString)
{
int i = int.Parse(theIntAsString);
_intField = i;
}
public Sample(int theInt) => _intField = theInt;
public int IntProperty => _intField;
private readonly int _intField;
}
Один конструктор получает значения напрямую, в то время как другой выполняет вычисления перед установкой полей. Мне нужно:
- Избегать дублирования кода инициализации полей
- Сохранять поля только для чтения (они должны быть установлены в конструкторе)
- Вызывать один конструктор из другого
Как правильно реализовать цепочку вызовов конструкторов в C# для совместного использования логики инициализации между конструкторами?
Цепочка вызовов конструкторов в C# позволяет вызывать один конструктор из другого с помощью синтаксиса this(), устраняя дублирование кода при инициализации полей только для чтения. Ключевой момент заключается в том, чтобы более сложный конструктор вызывал более простой после выполнения необходимых вычислений, обеспечивая корректную установку всех полей только для чтения во время создания объекта.
Содержание
- Основы цепочки вызовов конструкторов
- Синтаксис
this() - Реализация цепочки вызовов конструкторов с полями только для чтения
- Лучшие практики и шаблоны
- Первичные конструкторы в современном C#
- Типичные сценарии использования
Основы цепочки вызовов конструкторов
Цепочка вызовов конструкторов — это процесс вызова одного конструктора из другого в рамках того же класса или из базового класса. Как объясняется в блоге NashTech, “Цепочка вызовов конструкторов — это процесс вызова одного конструктора из другого в рамках того же класса или из базового класса”.
Этот метод служит нескольким важным целям:
- Повторное использование кода: Устраняет дублирование кода инициализации в разных конструкторах
- Согласованность: Обеспечивает единый шаблон инициализации для всех конструкторов
- Поддерживаемость: Изменения в логике инициализации нужно вносить только в одном месте
- Управление полями только для чтения: Корректно обрабатывает требования к инициализации полей только для чтения
Порядок выполнения имеет решающее значение: “Сначала вызывается конструктор, на который ссылается ключевое слово :this(), и если этот конструктор также ссылается на другой, он вызовет и его, поднимаясь по цепочке вызовов”, — говорится в Pluralsight.
Синтаксис this()
Синтаксис this() используется для вызова одного конструктора из другого в рамках того же класса. Синтаксис помещает вызов в начало определения конструктора, за которым следует двоеточие и ключевое слово this() с соответствующими параметрами.
public ClassName(parameters)
: this(otherParameters)
{
// Тело конструктора выполняется после завершения вызова цепочки
}
Основные правила цепочки вызовов конструкторов:
- Вызов
this()должен быть первым оператором в конструкторе - Можно вызвать только один конструктор (либо
this(), либоbase()) - Можно связать несколько конструкторов через цепочку вызовов
- Цепочка вызовов в конечном итоге должна достичь конструктора, который не вызывает другой
Как указано в Tutorial.TechAltum, “Это можно сделать с помощью ключевых слов this и base”.
Реализация цепочки вызовов конструкторов с полями только для чтения
Для вашего конкретного примера с полями только для чтения правильная реализация будет выглядеть так:
public class Sample
{
public Sample(string theIntAsString)
: this(int.Parse(theIntAsString))
{
// Дополнительная инициализация, специфичная для строки, при необходимости
}
public Sample(int theInt) => _intField = theInt;
public int IntProperty => _intField;
private readonly int _intField;
}
В этой реализации:
- Конструктор строки вызывает конструктор целого числа с помощью
: this(int.Parse(theIntAsString)) - Конструктор целого числа выполняет фактическое присвоение поля
- Оба конструктора правильно инициализируют поле
_intField - Отсутствует дублирование кода между конструкторами
В документации Microsoft Learn подтверждается, что “члены только для чтения могут быть присвоены только на уровне класса или в его конструкторе”, что соответствует данному подходу.
Также можно реализовать это наоборот, имея более специфичный конструктор вызывающий более общий:
public class Sample
{
public Sample(int theInt)
{
_intField = theInt;
}
public Sample(string theIntAsString)
: this(int.Parse(theIntAsString))
{
// Специфичная для строки логика здесь, при необходимости
}
public int IntProperty => _intField;
private readonly int _intField;
}
Оба подхода допустимы, но первый обычно предпочтительнее, когда один конструктор явно является более фундаментальным, чем другой.
Расширенный пример с несколькими полями только для чтения
Для более сложных сценариев с несколькими полями только для чтения:
public class ComplexSample
{
public ComplexSample(string data)
: this(ParseData(data))
{
// Дополнительная обработка, специфичная для строки
}
public ComplexSample(ParsedData parsedData)
{
_primaryField = parsedData.Value;
_secondaryField = CalculateSecondary(parsedData);
_timestampField = DateTime.UtcNow;
}
// Свойства
public int PrimaryProperty => _primaryField;
public string SecondaryProperty => _secondaryField;
public DateTime TimestampProperty => _timestampField;
private readonly int _primaryField;
private readonly string _secondaryField;
private readonly DateTime _timestampField;
private static ParsedData ParseData(string data)
{
// Логика разбора
return new ParsedData { Value = int.Parse(data) };
}
private static string CalculateSecondary(ParsedData data)
{
// Логика вычисления
return $"Значение: {data.Value}";
}
}
public class ParsedData
{
public int Value { get; set; }
}
Лучшие практики и шаблоны
При реализации цепочки вызовов конструкторов учитывайте эти лучшие практики:
1. Создавайте цепочку к наиболее общему конструктору
- Пусть более специфичные конструкторы вызывают более общие
- Это создает четкую иерархию логики инициализации
2. Выполняйте проверку данных на раннем этапе
- Выполняйте проверку параметров в конструкторе, который получает параметры
- Вызывайте следующий конструктор только после успешной проверки
public class ValidatedSample
{
public ValidatedSample(int value)
: this(value, ValidateValue(value))
{
}
public ValidatedSample(int value, string validationMessage)
{
_value = value;
_validationMessage = validationMessage;
}
private static string ValidateValue(int value)
{
if (value < 0)
throw new ArgumentException("Значение не может быть отрицательным");
return "Корректно";
}
private readonly int _value;
private readonly string _validationMessage;
}
3. Используйте закрытые конструкторы для общей инициализации
- Как предлагается на Stack Overflow, “вы можете создать закрытый конструктор, который может принимать оба типа аргументов, и ваши два исходных конструктора просто вызывают его, передавая null для отсутствующего аргумента. Это преимущество перед вызовом закрытых методов инициализации заключается в том, что это хорошо работает с полями только для чтения”
4. Рассмотрите шаблон объекта параметров для сложных конструкторов
- Когда конструкторы имеют много параметров, рассмотрите использование объекта параметров
public class ParameterizedSample
{
public ParameterizedSample(string data)
: this(new SampleParameters(data))
{
}
public ParameterizedSample(SampleParameters parameters)
{
_field1 = parameters.Value1;
_field2 = parameters.Value2;
}
private readonly int _field1;
private readonly string _field2;
}
public class SampleParameters
{
public SampleParameters(string data)
{
Value1 = int.Parse(data);
Value2 = data.ToUpper();
}
public int Value1 { get; }
public string Value2 { get; }
}
Первичные конструкторы в современном C#
C# 12 представил первичные конструкторы, которые обеспечивают более лаконичный синтаксис для цепочки вызовов конструкторов. Как объясняется в Microsoft Learn, “Любой другой конструктор для класса должен вызывать первичный конструктор, прямо или косвенно, через вызов конструктора this()”.
Вот как будет выглядеть ваш пример с первичными конструкторами:
public class Sample(int intField)
{
public Sample(string theIntAsString)
: this(int.Parse(theIntAsString))
{
}
public int IntProperty => intField;
}
Основные преимущества первичных конструкторов:
- Более лаконичный синтаксис
- Автоматическое создание поля/параметра
- Принудительная инициализация через цепочку вызовов
- Лучшая интеграция с другими языковыми функциями
Однако у первичных конструкторов есть некоторые ограничения:
- Все параметры доступны публично по умолчанию
- Проверка параметров может быть менее очевидной
- Могут не подходить для всех сценариев
Типичные сценарии использования
Цепочка вызовов конструкторов особенно полезна в этих сценариях:
1. Опциональные параметры и значения по умолчанию
public class OptionalParameterSample
{
public OptionalParameterSample(string name, int age = 18)
{
_name = name;
_age = age;
}
public OptionalParameterSample(string name)
: this(name, 18) // Возраст по умолчанию
{
}
private readonly string _name;
private readonly int _age;
}
2. Реализации фабричного шаблона
public class FactorySample
{
private FactorySample() { }
public static FactorySample CreateFromData(string data)
=> new FactorySample { _data = data };
public static FactorySample CreateFromFile(string filePath)
=> CreateFromData(File.ReadAllText(filePath));
private string _data;
}
3. Интеграция с шаблоном Builder
public class BuilderSample
{
public BuilderSample(Builder builder)
{
_field1 = builder.Field1;
_field2 = builder.Field2;
}
public static Builder CreateBuilder() => new Builder();
private readonly string _field1;
private readonly int _field2;
public class Builder
{
public string Field1 { get; set; }
public int Field2 { get; set; }
public BuilderSample Build() => new BuilderSample(this);
}
}
4. Сценарии обработки исключений
public class ExceptionHandlingSample
{
public ExceptionHandlingSample(string data)
: this(data, ValidateAndParse(data))
{
}
public ExceptionHandlingSample(string data, int parsedValue)
{
_data = data;
_value = parsedValue;
}
private static int ValidateAndParse(string data)
{
try
{
return int.Parse(data);
}
catch (FormatException ex)
{
throw new ArgumentException("Недопустимый формат данных", nameof(data), ex);
}
}
private readonly string _data;
private readonly int _value;
}
Заключение
Цепочка вызовов конструкторов с использованием синтаксиса this() — это правильный способ разделения логики инициализации между конструкторами в C# при соблюдении требований к полям только для чтения. Ключевые выводы:
- Используйте синтаксис
: this(parameters)для вызова одного конструктора из другого, убедившись, что это первый оператор - Создавайте цепочку к наиболее общему конструктору для формирования четкой иерархии инициализации
- Корректно обрабатывайте поля только для чтения, присваивая их значения только в конструкторах
- Рассмотрите первичные конструкторы в современном C# для более лаконичного синтаксиса
- Применяйте проверку данных и обработку ошибок в соответствующем конструкторе цепочки
Правильная реализация цепочки вызовов конструкторов устраняет дублирование кода, улучшает поддерживаемость и обеспечивает согласованную инициализацию объекта во всех перегрузках конструкторов.
Источники
- Как реализовать цепочку вызовов конструкторов в C# - Stack Overflow
- Цепочка вызовов конструкторов в C# | Pluralsight
- C# - Вызов конструктора из другого конструктора | makolyte
- Основы цепочки вызовов конструкторов в C# - NashTech Blog
- Цепочка вызовов конструкторов в C# - Tutorial.TechAltum
- Цепочка вызовов конструкторов – 2000 вещей, которые вы должны знать о C#
- Объявление первичных конструкторов C# – классы, структуры | Microsoft Learn
- Ключевое слово readonly - Справочник по C# | Microsoft Learn
- Конструкторы C# с примерами - Tutlane