У меня есть класс Wheel с двумя только инициализирующими свойствами Spokes и DiskBrake:
public class Wheel
{
internal Wheel() { }
public int Spokes { get; init; }
public bool DiskBrake { get; init; }
}
Для функциональности этого класса критически важно, чтобы эти свойства были неизменяемыми после создания экземпляра объекта Wheel. Поэтому крайне желательно, чтобы свойства были доступны только для инициализации и подкреплялись полями readonly.
Теперь у меня есть класс Bicycle, который содержит два экземпляра класса Wheel:
public class Bicycle
{
private readonly Wheel _frontWheel = new();
private readonly Wheel _rearWheel = new();
public int FrontWheelSpokes
{
get => _frontWheel.Spokes;
init => _frontWheel.Spokes = value;
}
public int RearWheelSpokes
{
get => _rearWheel.Spokes;
init => _rearWheel.Spokes = value;
}
public bool FrontWheelDiskBrake
{
get => _frontWheel.DiskBrake;
init => _frontWheel.DiskBrake = value;
}
public bool RearWheelDiskBrake
{
get => _rearWheel.DiskBrake;
init => _rearWheel.DiskBrake = value;
}
}
Предполагаемое использование этого класса:
Bicycle bicycle = new()
{
FrontWheelSpokes = 32,
RearWheelSpokes = 24,
FrontWheelDiskBrake = true,
RearWheelDiskBrake = false
};
К сожалению, этот код не компилируется. Я получаю четыре ошибки компиляции в строках init => _xxxWheel.Yyy = value;:
Свойство или индексатор «Wheel.Spokes», предназначенное только для инициализации, может быть назначено только в инициализаторе объекта или на «this» или «base» в конструкторе экземпляра или методе доступа «init».
Онлайн-демо.
Могу ли я что-нибудь сделать, чтобы исправить ошибки компиляции, не ставя под угрозу «только инициализацию» всех свойств в классах Wheel и Bicycle?
Ограничения:
Wheel внешнему миру. Только сам Bicycle должен создавать экземпляры своих колес.Bicycle оптимальна как есть. Я не хочу это менять.@MakePeaceGreatAgain все началось с того, что значения передавались через конструкторы. Проблема заключалась в том, что многие значения являются необязательными, что приводит к резкому увеличению количества конструкторов. Я начал с 6, потом в какой-то момент у меня стало 15, что стало большой нагрузкой как для тестирования, так и для использования. Затем я обнаружил свойства, доступные только для инициализации, и их количество снова сократилось до 6, и это здорово. Если бы я только мог решить эту проблему распространения только при инициализации... Но она кажется неразрешимой.
Я быстро придумал подход SomeBody, но эффективность слишком низкая, и как только свойства увеличиваются, он подвержен ошибкам (запись может быть полезна). На самом деле я считаю, что этот подход с конструктором более осуществим. Теперь, когда у нас есть именованные и дополнительные параметры, наличие 15 параметров не является большой проблемой.
@shingo Я использовал твою record идею в решении, которое опубликовал в качестве ответа ниже. Спасибо, что предложили свою помощь в этом вопросе!





Если вы не против выставить класс Wheel наружу, вам следует объявить свои свойства FrontWheel и RearWheel типа Wheel, и позволить этому классу самостоятельно обрабатывать свою инициализацию.
class Bicycle {
public Wheel FrontWheel { get; init; }
public Wheel RearWheel { get; init; }
}
// (...)
public static void Main() {
Bicycle bicycle = new() {
FrontWheel = new() { Spokes = 32, DiskBrake = true},
RearWheel = new() { Spokes = 24, DiskBrake = false }
};
}
Если вас волнует, что тип Wheel не виден на стороне вызова, вам придется сделать Bicycle владельцем интерфейса для инициализации и доступа к внутренним свойствам, принадлежащим Wheel.
Для этого вам нужно будет сделать эти свойства доступными только для получения и добавить конструктор Bicycle, в котором потребитель класса может инициализировать эти Wheel значения.
class Bicycle {
private readonly Wheel _frontWheel;
private readonly Wheel _rearWheel;
public int FrontWheelSpokes => _frontWheel.Spokes;
public int RearWheelSpokes => _rearWheel.Spokes;
public bool FrontWheelDiskBrake => _frontWheel.DiskBrake;
public bool RearWheelDiskBrake => _rearWheel.DiskBrake;
public Bicycle(int frontWheelSpokes, bool frontWheelDiskBrake, int rearWheelSpokes, bool rearWheelDiskBrake) {
_frontWheel = new() { Spokes = frontWheelSpokes, DiskBrake = frontWheelDiskBrake };
_rearWheel = new() { Spokes = rearWheelSpokes, DiskBrake = rearWheelDiskBrake };
}
}
// (...)
public static void Main() {
Bicycle bicycle = new(32, true, 24, false);
}
Спасибо Родриго за ответ. К сожалению, подвергать класс Wheel внешнему миру нежелательно. Wheel функционирует как внутренний компонент класса Bicycle, поэтому я могу изолировать код, относящийся к колесам, от кода, относящегося к остальной части велосипеда. Я мог бы поместить весь код в Bicycle и решить проблему только с инициализацией, но тогда Bicycle превратился бы в два объемистых кода. Также я хочу, чтобы значения инициализировались как свойства, а не как аргументы конструктора, чтобы минимизировать количество конструкторов Bicycle (их у меня уже много).
Кроме того, если я помещу весь код в Bicycle, я не смогу повторно использовать код, связанный с Wheel, в других моих классах, оснащенных колесами, таких как Handcart.
Я предлагаю пересмотреть ваш дизайн/ограничения. Свойство, предназначенное только для init, подлежит передаче только во время строительства. Если Wheel является частным, кто-то должен вызвать этот конструктор, и это Bicycle. Теперь, если вы хотите передать аргументы как присвоение свойства Bicycle, у вас возникает проблема: куда вызвать new Wheel()? В присвоении свойства FrontWheelSpokes? Или в FrontWheelBrake переуступке имущества? В Bycicle конструкторе? Помните: вы можете создать объект только один раз.
В настоящее время экземпляры Wheel создаются внутри конструктора Bicycle, и для компиляции моего кода я сделал свойство Wheel.Spokes изменяемым (get; set;). Что мне совсем не нравится. Похоже, что компилятор C# здесь не на моей стороне, что заставляет меня быть особенно осторожным, чтобы по ошибке не выстрелить себе в ногу.
Почему бы вам просто не создать свойства Wheel{ get; internal set; }? Вы сможете установить их вне конструктора, но только вы (без клиента вне сборки). И поскольку Wheel в любом случае не является публичным...
В моем проекте Wheel уже есть internal. Я боюсь не звонящих. Это я.
Ну, никакой магии здесь нет. Ваши варианты: 1) сделать тип свойств Wheel с помощью общедоступного конструктора или реквизитов инициализации; 2) сохранять Wheel внутренним и иметь в Bicycle соответствующие общедоступные свойства, которые устанавливаются в конструкторе Bicycle; 3) то же, что и выше, но установите для них определенные методы, такие как AddWheel(int spokes, bool disk), которые вы можете вызвать дважды после Bicycle построения; 4) создать эти Wheel свойства { get; internal set; }.
Родриго: Да, у меня много вариантов, и все они нежелательны. Это не твоя вина. Просто, судя по всему, компилятор C# на данный момент не способен оптимальным образом поддерживать мой сценарий.
Распространенный подход к сложным сценариям построения — написать фабричный метод или класс для инкапсуляции параметров построения.
Класс для инкапсуляции параметров конструкции для вашего примера может выглядеть так:
public class BicycleCreationData
{
public BicycleCreationData(int frontSpokes, bool frontDiskBrake, int rearSpokes, bool rearDiskBrake)
{
FrontSpokes = frontSpokes;
FrontDiskBrake = frontDiskBrake;
RearSpokes = rearSpokes;
RearDiskBrake = rearDiskBrake;
}
public int FrontSpokes { get; }
public bool FrontDiskBrake { get; }
public int RearSpokes { get; }
public bool RearDiskBrake { get; }
public int OptionalParameter1 { get; set; } = 42;
public string OptionalParameter2 { get; set; } = "42";
public double OptionalParameter3 { get; set; } = 4.2;
}
Необязательные данные задаются в конструкторе (вместо использования init), а необязательные данные просто используют свойства get/set.
При этом вы можете написать класс Bicycle следующим образом:
public class Bicycle
{
private readonly Wheel _frontWheel;
private readonly Wheel _rearWheel;
public Bicycle(BicycleCreationData data)
{
_frontWheel = new Wheel { DiskBrake = data.FrontDiskBrake, Spokes = data.FrontSpokes };
_rearWheel = new Wheel { DiskBrake = data.RearDiskBrake, Spokes = data.RearSpokes };
}
public int FrontWheelSpokes => _frontWheel.Spokes;
public int RearWheelSpokes => _rearWheel .Spokes;
public bool FrontWheelDiskBrake => _frontWheel.DiskBrake;
public bool RearWheelDiskBrake => _rearWheel .DiskBrake;
}
И вы могли бы создать Bicycle в два этапа следующим образом:
var bikeData = new BicycleCreationData(
frontSpokes: 32,
rearSpokes: 24,
frontDiskBrake: true,
rearDiskBrake: false)
{
OptionalParameter1 = 100,
OptionalParameter2 = "100"
};
var bike = new Bicycle(bikeData);
Я не уверен, что это то, что вы ищете, но это решает комбинационный взрыв конструкторов.
Нередко класс данных создания вкладывается в класс, который должен быть создан, а затем добавляется метод Create() к классу данных создания. Это позволяет классу данных создания иметь доступ к частным методам в создаваемом классе.
Например, вы можете сделать конструктор закрытым и заставить все создание выполняться через класс создания следующим образом (а также при желании добавить дополнительную проверку данных создания):
public class Bicycle
{
private readonly Wheel _frontWheel;
private readonly Wheel _rearWheel;
public sealed class Creator
{
public Creator(int frontSpokes, bool frontDiskBrake, int rearSpokes, bool rearDiskBrake)
{
FrontSpokes = frontSpokes;
FrontDiskBrake = frontDiskBrake;
RearSpokes = rearSpokes;
RearDiskBrake = rearDiskBrake;
}
public int FrontSpokes { get; }
public bool FrontDiskBrake { get; }
public int RearSpokes { get; }
public bool RearDiskBrake { get; }
public int OptionalParameter1 { get; set; } = 42;
public string OptionalParameter2 { get; set; } = "42";
public double OptionalParameter3 { get; set; } = 4.2;
public Bicycle Create()
{
// Add additional data validation here.
return new Bicycle(this);
}
}
private Bicycle(Creator data)
{
_frontWheel = new Wheel { DiskBrake = data.FrontDiskBrake, Spokes = data.FrontSpokes };
_rearWheel = new Wheel { DiskBrake = data.RearDiskBrake, Spokes = data.RearSpokes };
}
public int FrontWheelSpokes => _frontWheel.Spokes;
public int RearWheelSpokes => _rearWheel .Spokes;
public bool FrontWheelDiskBrake => _frontWheel.DiskBrake;
public bool RearWheelDiskBrake => _rearWheel .DiskBrake;
}
Тогда вы бы создали это так:
var bike = new Bicycle.Creator(
frontSpokes: 32,
rearSpokes: 24,
frontDiskBrake: true,
rearDiskBrake: false)
{
OptionalParameter1 = 100,
OptionalParameter2 = "100"
}
.Create();
Спасибо Мэтью за ответ. Вы поделились довольно многими хорошими идеями. К сожалению, мой сценарий не позволяет кардинально изменить способ построения экземпляров. Мне нужно использовать классический конструктор. Что касается вашего первого предложения, оно подразумевает, что некоторые свойства являются необязательными, а некоторые нет. В моем случае все свойства являются необязательными. Bicycle может быть создан со всеми свойствами, имеющими значения по умолчанию. Просто некоторые свойства используются реже, чем другие, и именно эти свойства я выставляю как доступные только для инициализации. Наиболее распространенные задаются в конструкторе.
In my case all properties are optional ... It's just that some properties are used more rarely than others, and these are the properties that I expose as init-only Я не понимаю, что вы имеете в виду... Свойство init НЕ является обязательным.
Вы уверены, что они обязательны? Я почти уверен, что это не так (демо).
Извините, вы правы, я думал о ключевом слове required .
Можно воссоздать экземпляр вашего свойства колеса в каждом установщике инициализации. Вам придется скопировать свойства, которые вы не установили. Это имеет тот серьезный недостаток, что вам придется вызывать множество ненужных конструкторов Wheel, но вы можете сохранить дизайн своего класса. Код может быть таким (для краткости, только с передним колесом):
class Bicycle
{
private readonly Wheel _frontWheel = new();
public int FrontWheelSpokes
{
get => _frontWheel.Spokes;
init => _frontWheel = new Wheel
{
Spokes = value,
DiskBrake = _frontWheel.DiskBrake
};
}
public bool FrontWheelDiskBrake
{
get => _frontWheel.DiskBrake;
init => _frontWheel = new Wheel
{
Spokes = _frontWheel.Spokes,
DiskBrake = value
};
}
}
Онлайн-демо: https://dotnetfiddle.net/LzMdP5
Спасибо SomeBody за ответ. Ваш ответ, безусловно, удовлетворяет требованиям моих вопросов, поэтому я приму его. Ваша идея очень похожа на идею Шинго (который, к сожалению, удалил свой ответ). Однако я не уверен, что буду использовать его в своем реальном проекте. Мне придется взвесить все за и против по сравнению с небезопасной, но более эффективной альтернативой, которую я сейчас использую.
В качестве альтернативы вы можете создать свойства Lazy и сохранить значения init в полях для использования при создании свойства Lazy.
Это требует дублирования всех свойств инициализации в полях, поэтому это несколько громоздко, но я публикую это просто ради интереса:
class Bicycle
{
private readonly Lazy<Wheel> _frontWheel;
private readonly Lazy<Wheel> _rearWheel;
private readonly int _initFrontWheelSpokes = 24;
private readonly int _initRearWheelSpokes = 32;
private readonly bool _initFrontWheelDiskBrake;
private readonly bool _initRearWheelDiskBrake = true;
public Bicycle()
{
_frontWheel = new Lazy<Wheel>(() => new Wheel { Spokes = _initFrontWheelSpokes, DiskBrake = _initFrontWheelDiskBrake });
_rearWheel = new Lazy<Wheel>(() => new Wheel { Spokes = _initRearWheelSpokes, DiskBrake = _initRearWheelDiskBrake });
}
public int FrontWheelSpokes
{
get => _frontWheel.Value.Spokes;
init => _initFrontWheelSpokes = value ;
}
public bool FrontWheelDiskBrake
{
get => _frontWheel.Value.DiskBrake;
init => _initFrontWheelDiskBrake = value;
}
public int RearWheelSpokes
{
get => _rearWheel.Value.Spokes;
init => _initRearWheelSpokes = value ;
}
public bool RearWheelDiskBrake
{
get => _rearWheel.Value.DiskBrake;
init => _initRearWheelDiskBrake = value;
}
}
Да, это тоже интересно, Мэтью, спасибо, что поделился. Однако в моем реальном сценарии я не могу позволить себе получить доступ к свойствам FrontWheelSpokes/FrontWheelDiskBrake и т. д. через поле volatile (Lazy<T>.Value). Мой настоящий класс Bicycle чувствителен к производительности и не предназначен для обеспечения потокобезопасности. А еще он чувствителен к памяти, поэтому все эти посторонние поля съедят мой бюджет памяти!
Если он чувствителен к производительности и памяти, то подход к воссозданию всех объектов (согласно принятому ответу) тоже может быть не таким уж хорошим!
Исполнение, указанное в принятом ответе, оплачивается только на этапе строительства. На последующую работу класса это не влияет. Я принял этот ответ, потому что он удовлетворяет вопросу, заданному в вопросе, и я ничего не упомянул там о производительности. Этот ответ также удовлетворяет требованиям, но я не могу принять два ответа. И SomeBody был первым! :-)
ОК, это достаточно справедливо! Но я должен отметить, что подход Lazy создает объекты только один раз - просто построение откладывается до первого доступа к свойству. Конечно, это снижает производительность, потому что фактически для каждого доступа к if (isCreated) есть Lazy<T>.Value. С точки зрения производительности было бы трудно сказать, какой подход является лучшим без инструментов. Кроме того, с точки зрения памяти стоимость дополнительных полей может быть не такой высокой, как создание нескольких объектов — опять же, только инструментарий может ответить на этот вопрос.
Это более сложная версия полезного ответа SomeBody , включающая идею Shingo о создании Wheel пластинки:
record class Wheel
{
internal Wheel() { }
public int Spokes { get; init; }
public bool DiskBrake { get; init; }
}
Теперь я могу реализовать Bicycle вот так:
class Bicycle
{
private readonly Wheel _frontWheel = new();
private readonly Wheel _rearWheel = new();
public int FrontWheelSpokes
{
get => _frontWheel.Spokes;
init => _frontWheel = _frontWheel with { Spokes = value };
}
public int RearWheelSpokes
{
get => _rearWheel.Spokes;
init => _rearWheel = _rearWheel with { Spokes = value };
}
public bool FrontWheelDiskBrake
{
get => _frontWheel.DiskBrake;
init => _frontWheel = _frontWheel with { DiskBrake = value };
}
public bool RearWheelDiskBrake
{
get => _rearWheel.DiskBrake;
init => _rearWheel = _rearWheel with { DiskBrake = value };
}
}
Требуется некоторое объяснение, поскольку в приведенном выше коде происходит много всего. Каждый раз, когда используется свойство Bicycle, предназначенное только для инициализации, создается поверхностная копия Wheel на основе существующего Wheel, хранящегося в частном поле Bicycle. Давайте посмотрим, например, как компилятор C# преобразует свойство FrontWheelSpokes:
public int FrontWheelSpokes
{
get
{
return _frontWheel.Spokes;
}
init
{
Wheel wheel = _frontWheel.<Clone>$();
wheel.Spokes = value;
_frontWheel = wheel;
}
}
<Clone>$ — это сгенерированный компилятором метод в классе Wheel:
[CompilerGenerated]
public virtual Wheel <Clone>$()
{
return new Wheel(this);
}
Конструктор клонирования также генерируется компилятором для класса Wheel:
[CompilerGenerated]
protected Wheel(Wheel original)
{
<Spokes>k__BackingField = original.<Spokes>k__BackingField;
<DiskBrake>k__BackingField = original.<DiskBrake>k__BackingField;
}
Вы можете полностью изучить сгенерированный компилятором код в Sharplab.
Так что же происходит, когда создается экземпляр Bicycle, как показано в вопросе?
Bicycle bicycle = new()
{
FrontWheelSpokes = 32,
RearWheelSpokes = 24,
FrontWheelDiskBrake = true,
RearWheelDiskBrake = false
};
Этот код вызывает создание экземпляров 6 объектов Wheel. Два из них изначально создаются во время построения Bicycle, когда инициализируются частные поля _frontWheel и _rearWheel. Дополнительные 4 создаются путем вызова свойств нового файла Bicycle, предназначенных только для инициализации. Из всех 6 экземпляров Wheel последние 2 сохраняются, а первые 4 отбрасываются и становятся пригодными для сбора мусора. Отброшенные объекты представляют собой мелкие копии, поэтому их объем памяти равен размеру default-инициализированного Wheel. Работа, необходимая для их создания, незначительна: всего лишь присвоение резервных полей оригинала клону. Тем не менее, этой работы можно было бы избежать, если бы компилятор C# был более полезен в автоматическом распространении свойств только для инициализации.
Я все еще не решил, буду ли я использовать эту технику в моем реальном сценарии. Я использую свойства только для инициализации для некоторых настроек моего класса Wheel, которые используются редко, поэтому на практике эта мизерная стоимость будет редко окупаться. Меня больше беспокоят последствия преобразования моего Wheel в record, потому что Wheel — это абстрактный базовый класс, и есть другие классы, производные от него. Так что я могу столкнуться с какой-нибудь другой стеной по дороге.
Все это умно, но я не могу представить себе сценарий, в котором это действительно желательно, а не просто иметь частного установщика. Я имею в виду, что сегодня это может иметь смысл, но будет ли он иметь смысл для тех, кто увидит этот код через год (даже для вас самих)? Возможно, это просто отражение моего опыта поддержки кода.
@RodrigoRodrigues, ты имеешь в виду internal сеттера, а не private. Проблема с установщиком internal, который я сейчас использую, заключается в том, что он позволяет свойству изменяться. И если он мутирует, произойдут очень плохие вещи. Чтобы мой класс Wheel работал правильно, свойства Spokes/DiskBrake ДОЛЖНЫ быть неизменяемыми. Это похоже на свойство Length массива. Если бы можно было изменить Length по ошибке, программы по всему миру вылетали бы из-за ArgumentOutOfRangeException.
если вы хотите, чтобы что-то было доступно только для инициализации, почему бы не поместить это в конструктор (что в любом случае делает за вас компилятор в поддержке свойства
init)? Итак, определите количество спиц в конструкции вашего велосипеда:var bike = new Bicycle(frontwheelSpokes, rearWheelSpokes, ...);