Что такое "в основном полный" (im) подход к изменчивости для С#? - программирование

Что такое "в основном полный" (im) подход к изменчивости для С#?

Поскольку неизменяемость не полностью испечена на С# до степени, указанной в F #, или полностью в рамках (BCL), несмотря на некоторую поддержку в CLR, то какое довольно полное решение для (im) изменчивости для С#?

Мой порядок предпочтения - это решение, состоящее из общих паттернов/принципов, совместимых с

  • отдельная библиотека с открытым исходным кодом с несколькими зависимостями
  • небольшое количество дополнительных/совместимых библиотек с открытым исходным кодом
  • что-то коммерческое

что

  • охватывает типы Lippert immutability
  • предлагает достойную работу (ту неопределенную, которую я знаю)
  • поддерживает сериализацию
  • поддерживает клонирование/копирование (глубокое/мелкое/частичное?)
  • чувствует себя естественным в таких сценариях, как DDD, шаблоны компоновщика, конфигурация и потоки.
  • предоставляет неизменяемые коллекции

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

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");
    // ...
}
4b9b3361

Ответ 1

Лучше ли было бы использовать F #, где вы хотите истинную неизменность?

Ответ 2

Используйте этот 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 с обычной семантикой копирования.

Ответ 3

Лично я не знаю каких-либо сторонних или предыдущих решений этой проблемы, поэтому извиняюсь, если я покрываю старые земли. Но, если бы я собирался реализовать какой-то стандарт неизменности для проекта, над которым я работал, я бы начал с чего-то вроде этого:

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> также централизует взаимосвязь между неизменностью и копиями; если вы позже захотите каким-то образом оптимизировать, например, кэшировать моментальный снимок до тех пор, пока не будете грязны, вам не нужно делать это во всех реализациях логики копирования.

Ответ 4

Если цель состоит в том, чтобы объекты, которые ведут себя как неразделенные изменчивые объекты, но которые могут быть разделены, если это будет способствовать повышению эффективности, я бы предложил иметь частный, изменяемый тип "фундаментальных данных". Хотя любой, кто имеет ссылку на объекты этого типа, сможет их мутировать, никакие такие ссылки никогда не покинут сборку. Все внешние манипуляции с данными должны выполняться через объекты-обертки, каждый из которых содержит две ссылки:

  • UnsharedVersion - содержит единственную ссылку на свой внутренний объект данных и может изменять ее
  • SharedImmutableVersion - содержит ссылку на объект данных, к которой не существует ссылок, кроме других полей SharedImmutableVersion; такие объекты могут иметь изменяемый тип, но на практике будут неизменными, поскольку никакие ссылки никогда не будут доступны для кода, который будет мутировать их.

Одно или оба поля могут быть заполнены; когда они заполняются, они должны ссылаться на экземпляры с идентичными данными.

Если предпринимается попытка мутировать объект через оболочку, а поле UnsharedVersion равно null, клон объекта в SharedImmutableVersion должен храниться в UnsharedVersion. Затем SharedImmutableCVersion следует очистить, и объект в UnsharedVersion мутировал по желанию.

Если делается попытка клонировать объект, а SharedImmutableVersion пуст, клон объекта в UnsharedVersion следует хранить в SharedImmutableVersion. Затем следует создать новую оболочку с пустым полем UnsharedVersion и поле SharedImmutableVersion, заполненное SharedImmutableVersion от оригинала.

Многократные клоны сделаны из объекта, прямо или косвенно, и объект не был мутирован между конструкцией этих клонов, все клоны будут ссылаться на один и тот же экземпляр объекта. Однако любой из этих клонов может быть мутирован, но не затрагивает других. Любая такая мутация создаст новый экземпляр и сохранит его в UnsharedVersion.