Ответ 1
Лучше ли было бы использовать F #, где вы хотите истинную неизменность?
Поскольку неизменяемость не полностью испечена на С# до степени, указанной в F #, или полностью в рамках (BCL), несмотря на некоторую поддержку в CLR, то какое довольно полное решение для (im) изменчивости для С#?
Мой порядок предпочтения - это решение, состоящее из общих паттернов/принципов, совместимых с
что
Я также хотел бы включить шаблоны, которые могут возникнуть у сообщества, которые не подходят в рамках, таких как выражая намерение мутирования через интерфейсы ( где оба клиента, которые не должны что-то менять и могут что-то изменить, могут делать это только через интерфейсы, а не для класса поддержки (да, я знаю, что это не истинная неизменность, но достаточная):
public interface IX
{
int Y{ get; }
ReadOnlyCollection<string> Z { get; }
IMutableX Clone();
}
public interface IMutableX: IX
{
new int Y{ get; set; }
new ICollection<string> Z{ get; } // or IList<string>
}
// generally no one should get ahold of an X directly
internal class X: IMutableX
{
public int Y{ get; set; }
ICollection<string> IMutableX.Z { get { return z; } }
public ReadOnlyCollection<string> Z
{
get { return new ReadOnlyCollection<string>(z); }
}
public IMutableX Clone()
{
var c = MemberwiseClone();
c.z = new List<string>(z);
return c;
}
private IList<string> z = new List<string>();
}
// ...
public void ContriveExample(IX x)
{
if (x.Y != 3 || x.Z.Count < 10) return;
var c= x.Clone();
c.Y++;
c.Z.Clear();
c.Z.Add("Bye, off to another thread");
// ...
}
Лучше ли было бы использовать F #, где вы хотите истинную неизменность?
Используйте этот T4 template Я собрал вместе, чтобы решить эту проблему. Он должен, как правило, соответствовать вашим потребностям в отношении любых видов непреложных объектов, которые вам нужно создать.
Не нужно идти с дженериками или использовать какие-либо интерфейсы. Для моих целей я не хочу, чтобы мои неизменные классы были конвертированы друг в друга. Почему ты? Какие общие черты они должны разделять, это означает, что они должны быть конвертируемыми друг к другу? Выполнение шаблона кода должно быть заданием генератора кода (или, еще лучше, системы с достаточно большим типом, позволяющей вам определять общие шаблоны кода, которых, к сожалению, нет у С#).
Вот пример, выводимый из шаблона, чтобы проиллюстрировать основную концепцию при воспроизведении (неважно, какие типы используются для свойств):
public sealed partial class CommitPartial
{
public CommitID ID { get; private set; }
public TreeID TreeID { get; private set; }
public string Committer { get; private set; }
public DateTimeOffset DateCommitted { get; private set; }
public string Message { get; private set; }
public CommitPartial(Builder b)
{
this.ID = b.ID;
this.TreeID = b.TreeID;
this.Committer = b.Committer;
this.DateCommitted = b.DateCommitted;
this.Message = b.Message;
}
public sealed class Builder
{
public CommitID ID { get; set; }
public TreeID TreeID { get; set; }
public string Committer { get; set; }
public DateTimeOffset DateCommitted { get; set; }
public string Message { get; set; }
public Builder() { }
public Builder(CommitPartial imm)
{
this.ID = imm.ID;
this.TreeID = imm.TreeID;
this.Committer = imm.Committer;
this.DateCommitted = imm.DateCommitted;
this.Message = imm.Message;
}
public Builder(
CommitID pID
,TreeID pTreeID
,string pCommitter
,DateTimeOffset pDateCommitted
,string pMessage
)
{
this.ID = pID;
this.TreeID = pTreeID;
this.Committer = pCommitter;
this.DateCommitted = pDateCommitted;
this.Message = pMessage;
}
}
public static implicit operator CommitPartial(Builder b)
{
return new CommitPartial(b);
}
}
Основной шаблон состоит в том, чтобы иметь неизменяемый класс с вложенным изменчивым классом Builder
, который используется для создания экземпляров неизменяемого класса изменчивым способом. Единственный способ установить неизменяемые свойства класса - это построить класс ImmutableType.Builder
и установить его в обычном изменяемом способе и преобразовать его в свой класс ImmutableType
с неявным оператором преобразования.
Вы можете расширить шаблон T4, чтобы добавить общедоступный ctor по умолчанию в класс ImmutableType
, чтобы избежать двойного распределения, если вы можете установить все свойства вверх.
Здесь пример использования:
CommitPartial cp = new CommitPartial.Builder() { Message = "Hello", OtherFields = value, ... };
или...
CommitPartial.Builder cpb = new CommitPartial.Builder();
cpb.Message = "Hello";
...
// using the implicit conversion operator:
CommitPartial cp = cpb;
// alternatively, using an explicit cast to invoke the conversion operator:
CommitPartial cp = (CommitPartial)cpb;
Обратите внимание, что в задании используется оператор неявного преобразования от CommitPartial.Builder
до CommitPartial
. Эта часть, которая "замораживает" изменчивый CommitPartial.Builder
, создавая из него неизменяемый экземпляр CommitPartial
с обычной семантикой копирования.
Лично я не знаю каких-либо сторонних или предыдущих решений этой проблемы, поэтому извиняюсь, если я покрываю старые земли. Но, если бы я собирался реализовать какой-то стандарт неизменности для проекта, над которым я работал, я бы начал с чего-то вроде этого:
public interface ISnaphot<T>
{
T TakeSnapshot();
}
public class Immutable<T> where T : ISnaphot<T>
{
private readonly T _item;
public T Copy { get { return _item.TakeSnapshot(); } }
public Immutable(T item)
{
_item = item.TakeSnapshot();
}
}
Этот интерфейс будет реализован примерно так:
public class Customer : ISnaphot<Customer>
{
public string Name { get; set; }
private List<string> _creditCardNumbers = new List<string>();
public List<string> CreditCardNumbers { get { return _creditCardNumbers; } set { _creditCardNumbers = value; } }
public Customer TakeSnapshot()
{
return new Customer() { Name = this.Name, CreditCardNumbers = new List<string>(this.CreditCardNumbers) };
}
}
И код клиента будет выглядеть примерно так:
public void Example()
{
var myCustomer = new Customer() { Name = "Erik";}
var myImmutableCustomer = new Immutable<Customer>(myCustomer);
myCustomer.Name = null;
myCustomer.CreditCardNumbers = null;
//These guys do not throw exceptions
Console.WriteLine(myImmutableCustomer.Copy.Name.Length);
Console.WriteLine("Credit card count: " + myImmutableCustomer.Copy.CreditCardNumbers.Count);
}
Вопиющий недостаток заключается в том, что реализация так же хороша, как и клиент ISnapshot
реализации TakeSnapshot
, но, по крайней мере, он будет стандартизировать вещи, и вы знаете, куда искать, если у вас есть проблемы, связанные с сомнительными изменчивость. Бремя также будет заключаться в том, чтобы потенциальные разработчики могли определить, могут ли они обеспечить неизменность моментального снимка и не реализовать интерфейс, если нет (т.е. класс возвращает ссылку на поле, которое не поддерживает какой-либо тип клона/копии и, следовательно, не может быть снимок-е изд).
Как я уже сказал, это начало - как бы я мог начать - конечно, не оптимальное решение или законченная, отполированная идея. Отсюда я увижу, как мое использование эволюционировало и соответствующим образом изменило этот подход. Но, по крайней мере, здесь я бы знал, что могу определить, как сделать что-то неизменное и написать модульные тесты, чтобы убедиться, что это так.
Я понимаю, что это далеко не просто реализация копии объекта, но она стандартизирует копию по неизменяемости. В базе кода вы можете увидеть некоторых разработчиков ICloneable
, некоторые конструкторы копирования и некоторые явные методы копирования, возможно, даже в одном классе. Определение чего-то вроде этого говорит вам о том, что намерение конкретно связано с неизменяемостью - я хочу моментальный снимок, а не дублированный объект, потому что мне захочется больше этого объекта. Класс Immtuable<T>
также централизует взаимосвязь между неизменностью и копиями; если вы позже захотите каким-то образом оптимизировать, например, кэшировать моментальный снимок до тех пор, пока не будете грязны, вам не нужно делать это во всех реализациях логики копирования.
Если цель состоит в том, чтобы объекты, которые ведут себя как неразделенные изменчивые объекты, но которые могут быть разделены, если это будет способствовать повышению эффективности, я бы предложил иметь частный, изменяемый тип "фундаментальных данных". Хотя любой, кто имеет ссылку на объекты этого типа, сможет их мутировать, никакие такие ссылки никогда не покинут сборку. Все внешние манипуляции с данными должны выполняться через объекты-обертки, каждый из которых содержит две ссылки:
Одно или оба поля могут быть заполнены; когда они заполняются, они должны ссылаться на экземпляры с идентичными данными.
Если предпринимается попытка мутировать объект через оболочку, а поле UnsharedVersion равно null, клон объекта в SharedImmutableVersion должен храниться в UnsharedVersion. Затем SharedImmutableCVersion следует очистить, и объект в UnsharedVersion мутировал по желанию.
Если делается попытка клонировать объект, а SharedImmutableVersion пуст, клон объекта в UnsharedVersion следует хранить в SharedImmutableVersion. Затем следует создать новую оболочку с пустым полем UnsharedVersion и поле SharedImmutableVersion, заполненное SharedImmutableVersion от оригинала.
Многократные клоны сделаны из объекта, прямо или косвенно, и объект не был мутирован между конструкцией этих клонов, все клоны будут ссылаться на один и тот же экземпляр объекта. Однако любой из этих клонов может быть мутирован, но не затрагивает других. Любая такая мутация создаст новый экземпляр и сохранит его в UnsharedVersion.