3

我已经将不可变类型存储在临时 CQRS 读取存储中(查询/读取端,实际上是由一个带有抽象访问层的简单列表实现的,此时我不想使用完整的文档数据库)。这些读取存储包含如下项目:

public class SomeItem
{
    private readonly string name;
    private readonly string description;

    public SomeItem(string name, string description)
    {
        this.name = name;
        this.description = description;
    }

    public string Name
    {
        get { return this.name; }
    }

    public string Description
    {
        get { return this.description; }
    }
}

现在我想更改名称并在第二个命令中更改描述。这些更改应保持当前状态,这意味着对于上面的示例:

// initial state
var someItem = new SomeItem("name", "description");

// update name -> newName
someItem = new SomeItem("newName", someItem.Description);

// update description -> newDescription
someItem = new SomeItem(someItem.Name, "newDescription");

如果您有多个属性,这对我来说确实很容易出错……您必须设法保持当前状态。我可以为每种类型添加类似 Clone() 的东西,但我认为/希望那里有更好的东西,性能良好且易于使用,我不想编写太多重复代码(懒惰的程序员)。有什么建议可以改进上面的代码吗?SomeItem 类需要保持不变(通过几个不同的线程传输)。

4

5 回答 5

4

可悲的是,C# 中没有简单的方法。F# 有with关键字,你可以看一下 lens,但在 C# 中这一切都有些乏味。我能给你的最好的东西是这样的:

class SomeItem
{
  private readonly string name;
  private readonly string description;

  public SomeItem(string name, string description)
  {
    this.name = name;
    this.description = description;
  }

  public SomeItem With
    (
      Option<string> name = null,
      Option<string> description = null
    )
  {
    return new SomeItem
      (
        name.GetValueOrDefault(this.name), 
        description.GetValueOrDefault(this.description)
      );
  }
}

这使您可以进行更新,例如

var newItem = oldItem.With(name: "My name!");

我已经将这种方法与扩展方法和 T4 一起使用,效果很好,但是即使您手动编写代码,它也相当可靠 - 如果您添加一个新字段,您也必须将它添加到该字段中With,因此它工作得很好。

如果您愿意容忍运行时代码生成和降低类型安全性,还有其他一些方法,但这有点违背 IMO 的原则。

于 2016-08-05T12:53:34.683 回答
4

在 C#9 中,我们为此使用了with运算符。

   public record Car
    {
        public string Brand { get; init; }   
        public string Color { get; init; }    
    }
    var car = new Car{ Brand = "BMW", Color = "Red" }; 
    var anotherCar = car with { Brand = "Tesla"};

With-expressions 处理不可变数据时,一种常见的模式是从现有值创建新值来表示新状态。例如,如果我们的人要更改他们的姓氏,我们会将其表示为一个新对象,该对象是旧对象的副本,但姓氏不同。这种技术通常被称为非破坏性突变。记录不是随时间表示该人,而是表示该人在给定时间的状态。为了帮助这种编程风格,记录允许一种新的表达方式。with 表达式:

C#9 中的新闻

注意 With 运算符仅受记录支持。

记录经典面向对象编程的核心是对象具有强标识并封装随时间演变的可变状态的思想。C# 一直在这方面工作得很好,但有时你想要的恰恰相反,这里 C# 的默认设置往往会妨碍你,让事情变得非常费力。

C#9 中的记录

于 2021-01-21T11:06:24.847 回答
3

您要查找的内容通常称为with 运算符

// returns a new immutable object with just the single property changed
someItem = { someItem with Name = "newName" };

不幸的是,与 F# 不同,C# 没有这样的运算符(还没有?)。

其他 C# 开发人员也缺少此功能,这就是为什么有人编写了 Fody 扩展来做到这一点

这是另一种方法,它UpdateWith手动实现一个方法,但需要一个Option<T>辅助类。Luaan 的回答更详细地描述了这种方法:

于 2016-08-05T12:31:00.883 回答
0

简单的解决方案

我也想过这个问题。记录不适合我的目的,因为它需要与 EF Core 交互。

我建议一种简单且低成本的方法:

  • 向类添加一个复制构造函数;
  • 使克隆期间更改的属性可用于初始化;
  • 通过具有初始化列表的复制构造函数克隆具有更改的对象:
var a = new SomeItem("name", "abracadabra");
var b = new SomeItem(a) {Description="descr"};

简单的代码

var a = new SomeItem("name", "abracadabra");
var b = new SomeItem(a) {Description="descr"};

public class SomeItem
{
    private string name;
    private string description;

    public SomeItem(string name, string description)
    {
        Name = name;
        Description = description;
    }

    public SomeItem(SomeItem another): this(another.Name, another.Description)
    {
    }

    public string Name
    {
        get => name;
        init => name = value;
    }

    public string Description
    {
        get => description;
        init => description = value;
    }
}

扩展解决方案

如果在编译时不知道最终类型,那么这种方法很容易扩展。假设有一个类“ValueObject”,我们需要克隆它的派生类型。

注意:对于某些地方的错误翻译,我深表歉意。使用 google.translate 获得的英文版

附加代码

using System.Linq.Expressions;
using Light.GuardClauses;
using JetBrains.Annotations;
using static DotNext.Linq.Expressions.ExpressionBuilder;

using ValueObject = Company.Domain....;


/// <summary>
/// The plagiarizer creates a copy of the object with a change in its individual properties using an initializer
/// </summary>
/// <remarks> The foreign object must define a copy constructor, and mutable members must support initialization </remarks>
public struct Plagiarist {
    /// <summary>
    /// Object to be copied
    /// </summary>
    private readonly object _alienObject;

    /// <summary>
    /// Type <see cref="_alienObject" />
    /// </summary>
    private Type _type => _alienObject.GetType();

    /// <summary>
    /// Object parsing Expression
    /// </summary>
    private readonly ParsingInitializationExpression _parser = new();

    public Plagiarist(object alienObject) {
        _alienObject = alienObject.MustNotBeNullReference();
        if (!CopyConstructorIs())
            throw new ArgumentException($"Type {_type.FullName} must implement a copy constructor");
    }

    /// <summary>
    /// Does the object we want to plagiarize have a copy constructor?
    /// </summary>
    /// <returns>True - there is a copy constructor, otherwise - false</returns>
    [Pure]
    private bool CopyConstructorIs() {
        return _type.GetConstructor(new[] { _type }) is not null;
    }

    /// <summary>
    /// Returns a copy of a foreign object with a change in its individual properties using an initializer
    /// </summary>
    /// <param name="initializer">
    /// <see cref="Expression" /> create an object with initialization of those fields,
    /// which need to be changed:
    /// <code>() => new T() {Member1 = "Changed value1", Member2 = "Changed value2"}</code>
    /// or <see cref="Expression" /> create an anonymous type with initialization of those fields
    /// that need to be changed:
    /// <code>() => new {Member1 = "Changed value1", Member2 = "Changed value2"}</code>
    /// </param>
    /// <returns></returns>
    [Pure]
    public object Plagiarize(Expression<Func<object>> initializer) {
        var (newValues, constructParam) = _parser.ParseInitialization(initializer);
        var constrCopies = _type.New(_alienObject.Const().Convert(_type));

        Expression plagiarist = (newValues.Count, constructParam.Count) switch {
            (> 0, _) => Expression.MemberInit(constrCopies, newValues.Values),
            (0, > 0) => Expression.MemberInit(constrCopies, ConstructorInInitializationList(constructParam).Values),
            _ => constrCopies
        };

        var plagiarize = Expression.Lambda<Func<object>>(plagiarist).Compile();

        return plagiarize();
    }

    [Pure]
    public Dictionary<string, MemberAssignment> ConstructorInInitializationList(
        Dictionary<string, Expression> constructorParameters) {
        Dictionary<string, MemberAssignment> initializer = new();
        const BindingFlags flagReflections = BindingFlags.Default | BindingFlags.Instance | BindingFlags.Public;
        var allProperties = _type.GetProperties(flagReflections);
        var allFields = _type.GetFields(flagReflections);

        foreach (var memberName in constructorParameters.Keys) {
            var property = allProperties.FirstOrDefault(s => s.Name ==memberName);
            var field = allFields.FirstOrDefault(s => s.Name == memberName);
            (MemberInfo member, Type memberType) = (property, field) switch {
                ({ }, _) => (property, property.PropertyType),
                (null, { }) => ((MemberInfo)field, field.FieldType),
                _ => throw new ArgumentException($"{_type.FullName} does not contain member {memberName}")
            };

            initializer[memberName] = Expression.Bind(member, constructorParameters[memberName].Convert(memberType));
        }

        return initializer;
    }
    
    /// <summary>
    /// Template "Visitor" for traversing the expression tree in order to highlight
    /// initialization expression and constructor
    /// </summary>
    private class ParsingInitializationExpression : ExpressionVisitor {
        private Dictionary<string, MemberAssignment>? _initializer;
        private Dictionary<string, Expression>? _initializerAnonym;

        /// <summary>
        /// Parses the expression tree and returns the initializer and constructor parameters
        /// </summary>
        /// <param name="initializer"><see cref="Expression" /> to parse</param>
        /// <returns> tuple of initializer and constructor</returns>
        public ParsedInitialization ParseInitialization(Expression initializer) {
            _initializer = new Dictionary<string, MemberAssignment>();
            _initializerAnonym = new Dictionary<string, Expression>();
            Visit(initializer);
            return new ParsedInitialization(_initializer, _initializerAnonym);
        }

        protected override MemberAssignment VisitMemberAssignment(MemberAssignment node) {
            _initializer![node.Member.Name] = node;
            return base.VisitMemberAssignment(node);
        }

        protected override Expression VisitNew(NewExpression node) {
            foreach (var (member, value) in node.Members?.Zip(node.Arguments) ??
                                             Array.Empty<(MemberInfo First, Expression Second)>())
                _initializerAnonym![member.Name] = value;

            return base.VisitNew(node);
        }

        /// <summary>
        /// Type to return values from method <see cref="ParseInitialization" />
        /// </summary>
        /// <param name="Initializer"></param>
        /// <param name="ConstructorParameters"></param>
        public record struct ParsedInitialization(Dictionary<string, MemberAssignment> Initializer,
            Dictionary<string, Expression> ConstructorParameters);
    }
}

public static class ValueObjectPlagiarizer{
    /// <summary>
    /// Creates a copy of the object with a change in its individual properties using an initializer
    /// </summary>
    /// <param name="alien">Object to be plagiarized</param>
    /// <param name="initializer">
    /// <see cref="Expression" /> creating an object of type <typeparamref name="T" />
    /// with initialization of those fields that need to be changed:
    /// <code>ob.Plagiarize(() => new T() {Member1 = "Changed value1", Member2 = "Changed value2"})</code>
    /// or <see cref="Expression" /> create an anonymous type with initialization of those fields that need to be changed:
    /// <code>ob.Plagiarize(() => new {Member1 = "Changed value1", Member2 = "Changed value2"})</code>
    /// </param>
    /// <returns>plagiarism of the object</returns>
    public static object Plagiarize<T>(this ValueObject alien, Expression<Func<T>> initializer)
        where T : class {
        var bodyReduced = initializer.Convert<object>();
        var initializerReduced = Expression.Lambda<Func<object>>(bodyReduced, initializer.Parameters);

        return new Plagiarist(alien).Plagiarize(initializerReduced);
    }
} 

用法

如果 SomeItem 是 ValueObject 的后代

ValueObject a = new SomeItem("name", "abracadabra");

// via type constructor
var b = (SomeItem)a.Plagiarize(()=>new SomeItem(null){Description="descr"});
// anonymous type 
var c = (SomeItem)a.Plagiarize(()=>new{Description="descr"});

b.Description.Should().Be("descr"); //true
c.Description.Should().Be("descr"); //true
于 2022-02-18T18:49:59.380 回答
-2

如果您想要做的是(正如您评论的那样)更新现有对象的名称,那么 readonly 属性可能是糟糕的设计。否则,如果它真的是您想要创建的新对象,您可能希望您的类使用“Dispose”方法实现一些接口。

于 2016-08-05T12:31:16.303 回答