Возможно ли реализовать метод, который клонирует объект и применяет изменения к частным свойствам (без отражения)?

#c# #domain-driven-design

#c# #domain-driven-design

Вопрос:

Объекты значений в DDD неизменяемы, свойства обычно устанавливаются один раз с помощью конструктора.

Иногда мне требуется копия объекта value только с некоторыми изменениями, например, десять свойств должны быть скопированы, и одно свойство получит новое значение. В этом случае я хочу избежать использования конструктора с одиннадцатью параметрами, вместо этого я хочу реализовать метод, который возвращает копию, но в то же время применяет некоторые изменения к свойствам. Я знаю, что могу сделать это с помощью отражения, но хотел бы проверить, можно ли избежать отражения.

 public class Foo
{
    public int Bar { get; set; } // actual use case is { get; private set; }
    
    public Foo(int bar)
    {
        Bar = bar;
    }
    
    public Foo CloneAndApply(Action<Foo> apply)
    {
        var result = new Foo(Bar);
        apply(result);
        return resu<
    }
}
 

Это работает, потому что свойство «Bar» имеет общедоступный установщик. Мне нужно что-то, что позволяет устанавливать «Bar», когда он является закрытым членом.

 var test = new Foo(1);
var clone = test.CloneAndApply(x => x.Bar = 2);
Console.WriteLine(clone.Bar);
 

Комментарии:

1. public Foo CloneAndApply(int bar) Не будет работать? Я знаю, что ваш пример, скорее всего, является упрощенной версией того, что вам нужно, но, может быть, вам просто нужно несколько разных конструкторов?

2. @tymtam Да, это сработало бы, но оставляет меня с уродливым количеством параметров для CloneAndApply, когда я использую много свойств — плюс мне придется менять подпись каждый раз, когда свойство будет добавлено или удалено

Ответ №1:

Я получаю эти записи pre .net5 с WithX помощью методов, которые передают свойство, которое должно быть другим. Это не сложно, но это работает.

 public class Foo
{
    public int X { get; private set; }
    
    public int Bar { get; private set; }
    
    public Foo(int x, int bar) { X = x; Bar = bar; }
    
    public Foo WithBar(int bar) => new Foo(X, bar);
    }
}
 

Комментарии:

1. Это то, что делает Roslyn, FWIW

Ответ №2:

Вас могут заинтересовать записи, введенные в C # 9. Записи содержат много частей, но одной из них является поддержка «withers«, которые взаимодействуют с новыми свойствами, доступными только для инициализации, что позволяет легко создавать измененные копии записей.

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

 public record Foo
{
    public int Bar { get; init; }
    
    public Foo(int bar)
    {
        Bar = bar;
    }
}
 

и может использоваться как:

 var foo = new Foo(3);
var foo2 = foo with { Bar = 4 };
 

Эта запись также автоматически реализует равенство, основанное на равенстве ее отдельных членов, и переопределенную ToString реализацию.

Долгосрочный план состоит в том, чтобы разрешить использование withers с типами, отличными от записей, хотя (начиная с C # 9) это еще не поддерживается.


Если вы воспользуетесь преимуществами первичных конструкторов, вы сможете написать свою запись еще более лаконично:

 public record Foo(int Bar); 
 

Это автоматически генерирует Bar свойство с get init помощью средств доступа и и конструктора, который ему присваивает.

Комментарии:

1. Прекрасно, одна из причин начать использовать .NET 5 — жаль, что в настоящее время я застрял с Core 3.1. Я думаю, что это ответ, поскольку, вероятно, нет способа сделать это без отражения и C # < 9

2. Вы можете использовать это в .NET Core 3.1, если вы используете .NET 5 SDK и задаете <LangVersion>9</LangVersion> в своем csproj. Вам также понадобится namespace System.Runtime.CompilerServices { internal static class IsExternalInit { } } где-нибудь в вашем проекте.

3. Полезно знать — я работаю в команде, и это не случайное изменение, которое я могу внести, но, похоже, влияние можно контролировать

Ответ №3:

Другим решением может быть отражение. Это также можно использовать для присвоения значений частным свойствам:

     class Program
    {
        static void Main(string[] args)
        {
            var test = new Foo(1);
            var clone = test.CloneAndApply(x => x.GetType().GetProperty("Bar").SetValue(x, 2));
            Console.WriteLine(clone.Bar);
        }
    }

    public class Foo
    {
        public int Bar { get; private set; }

        public Foo(int bar)
        {
            Bar = bar;
        }

        public Foo CloneAndApply(Action<Foo> apply)
        {
            var result = new Foo(Bar);
            apply(result);
            return resu<
        }
    }
 

Комментарии:

1. Не то, что я ищу, поскольку он использует отражение, но все же хороший пример того, как это сделать с отражением

2. @TvdH А, понятно. Извините. Просто из интереса, почему без отражения? Есть ли какая-то конкретная причина?