在C#中模拟F#`with`关键字

时间:2015-08-24 04:03:17

标签: c# f#

有没有办法在C#中模拟F#' with个关键字?我知道它可能不会那么优雅,但是我想知道是否有办法处理创建新的不可变数据结构副本。

F#中的记录是detailed here

以下是我尝试做的一个例子。我们将创建" immutable"通过接口查看数据,同时保持具体类的可变性。这让我们可以在本地进行变异(在工作时),然后返回一个不可变的接口。这就是我们在C#中处理不变性的原因。

public interface IThing
{
    double A { get; }
    double B { get; }
}

public class Thing : IThing
{
    double A { get; set; }
    double B { get; set; }
}

然而,当需要对数据进行更改时,它不是非常类型(或可变性!)安全地来回投射,而且手动操作也是一个真正的痛苦将类的每个属性转换为新实例。如果我们添加一个新的怎么办?我是否必须追踪每次操纵?当我真的只需要what I had before, but with [some change]时,我不想创造未来的头痛。

示例:

// ...

IThing item = MethodThatDoesWork();

// Now I want to change it... how? This is ugly and error/change prone:
IThing changed = new Thing {
    A = item.A,
    B = 1.5
};

// ...

实现这一目标的合理策略是什么?你过去用过什么?

3 个答案:

答案 0 :(得分:8)

由于没有语法糖,我知道你必须要么:

  • 手工完成(见下文)
  • 使用一些反射/自动播放器(不是它的粉丝)
  • 使用一些AOP技术(既不是那些人的粉丝)

至少这是我现在能想到的。

我不认为最后两个是个好主意,因为你带来了大型机器来解决一个非常容易的问题。

是的,当你有数以千计的数据结构时,你可能会重新考虑这个,但是如果你只有几个数据结构,我就不会使用它。

所以剩下的基本上是智能构造函数和类似的东西 - 这是一个如何做到的简单示例(注意你并不需要所有这些 - 选择) - 它基本上是错过使用null / nullable寻找你需要的东西 - 更好的选择可能是重载或类似Option<T>数据类型的东西但是现在我觉得你得到了它:

class MyData
{
    private readonly int _intField;
    private readonly string _stringField;

    public MyData(int intField, string stringField)
    {
        _intField = intField;
        _stringField = stringField;
    }

    public MyData With(int? intValue = null, string stringValue = null)
    {
        return new MyData(
            intValue ?? _intField,
            stringValue ?? _stringField);
    }

    // should obviously be put into an extension-class of some sort
    public static MyData With(/*this*/ MyData from, int? intValue = null, string stringValue = null)
    {
        return from.With(intValue, stringValue);
    }

    public int IntField
    {
        get { return _intField; }
    }

    public string StringField
    {
        get { return _stringField; }
    }
}

答案 1 :(得分:6)

要添加到Carsten的正确答案,在C#中无法执行此操作,因为它不在语言中。在F#中,它是一种语言功能,succinct record declaration syntax expands to quite a bit of IL。 C#没有那种语言功能。

这是我不再喜欢在C#中工作的原因之一,因为与在F#中做同样的事情相比,开销太大了。尽管如此,有时我 在C#中工作的原因有一个或另一个,当发生这种情况时,我会咬紧牙关并手工写下记录。

例如,整个AtomEventSource库是用C#编写的,但是带有不可变的记录。这是AtomLink class的缩写示例:

public class AtomLink : IXmlWritable
{
    private readonly string rel;
    private readonly Uri href;

    public AtomLink(string rel, Uri href)
    {
        if (rel == null)
            throw new ArgumentNullException("rel");
        if (href == null)
            throw new ArgumentNullException("href");

        this.rel = rel;
        this.href = href;
    }

    public string Rel
    {
        get { return this.rel; }
    }

    public Uri Href
    {
        get { return this.href; }
    }

    public AtomLink WithRel(string newRel)
    {
        return new AtomLink(newRel, this.href);
    }

    public AtomLink WithHref(Uri newHref)
    {
        return new AtomLink(this.rel, newHref);
    }

    public override bool Equals(object obj)
    {
        var other = obj as AtomLink;
        if (other != null)
            return object.Equals(this.rel, other.rel)
                && object.Equals(this.href, other.href);

        return base.Equals(obj);
    }

    public override int GetHashCode()
    {
        return
            this.Rel.GetHashCode() ^
            this.Href.GetHashCode();
    }

    // Additional members removed for clarity.
}

除了必须输入所有这些的开销之外,如果你正在做(教条式)测试驱动开发(你没有 ),那也一直困扰着我,你也想测试这些方法。

但是,使用像AutoFixtureSemanticComparison这样的工具,你可以使它有些声明。这是一个example from AtomLinkTests

[Theory, AutoAtomData]
public void WithRelReturnsCorrectResult(
    AtomLink sut,
    string newRel)
{
    AtomLink actual = sut.WithRel(newRel);

    var expected = sut.AsSource().OfLikeness<AtomLink>()
        .With(x => x.Rel).EqualsWhen(
            (s, d) => object.Equals(newRel, d.Rel));
    expected.ShouldEqual(actual);
}

在这里,它仍然相对冗长,但您可以轻松地将其重构为通用方法,以便每个测试用例成为一个单行。

这仍然很麻烦,所以即使您在C#中编写了大部分代码,也可以考虑在单独的F#库中定义不可变类型。从C#看,F#记录看起来像“普通”不可变类,如上面的AtomLink。与其他一些F#类型(如有区别的联合)相反,F#记录完全可以从C#中获取。

答案 2 :(得分:0)

这是我尝试通过具体类在C#中模拟不可变突变。通过泛型的一些魔术,包括类型安全!

class Program
{
    static void Main(string[] args)
    {
        var r = new Random();

        // A new class item
        IDataItem item = new DataItem
        {
            A = r.NextDouble(),
            B = r.NextDouble(),
            C = r.NextDouble(),
            D = r.NextDouble()
        };

        // Type hinting here helps with inference
        // The resulting `newItem` is an "immutable" copy of the source item
        IDataItem newItem = item.With((DataItem x) =>
        {
            x.A = 0;
            x.C = 2;
        });

        // This won't even compile because Bonkers doesn't implement IDataItem!
        // No more casting madness and runtime errors!
        IBonkers newItem2 = item.With((Bonkers x) => { /* ... */ });
    }
}

// A generic record interface to support copying, equality, etc...
public interface IRecord<T> : ICloneable,
                              IComparable,
                              IComparable<T>,
                              IEquatable<T>
{
}

// Immutable while abstract
public interface IDataItem : IRecord<IDataItem>
{
    double A { get; }
    double B { get; }
    double C { get; }
    double D { get; }
}

// Mutable while concrete
public class DataItem : IDataItem
{
    public double A { get; set; }
    public double B { get; set; }
    public double C { get; set; }
    public double D { get; set; }

    public object Clone()
    {
        // Obviously you'd want to be more explicit in some cases (internal reference types, etc...)
        return this.MemberwiseClone();
    }

    public int CompareTo(object obj)
    {
        // Boilerplate...
        throw new NotImplementedException();
    }

    public int CompareTo(IDataItem other)
    {
        // Boilerplate...
        throw new NotImplementedException();
    }

    public bool Equals(IDataItem other)
    {
        // Boilerplate...
        throw new NotImplementedException();
    }
}

// Extension method(s) in a static class!
public static class Extensions
{
    // Generic magic helps you accept an interface, but work with a concrete type
    // Note how the concrete type must implement the provided interface! Type safety!
    public static TInterface With<TInterface, TConcrete>(this TInterface item, Action<TConcrete> fn)
        where TInterface : class, ICloneable
        where TConcrete : class, TInterface
    {
        var n = (TInterface)item.Clone() as TConcrete;
        fn(n);
        return n;
    }
}

// A sample interface to show type safety via generics
public interface IBonkers : IRecord<IBonkers> { }

// A sample class to show type safety via generics
public class Bonkers : IBonkers
{
    public object Clone()
    {
        throw new NotImplementedException();
    }

    public int CompareTo(object obj)
    {
        throw new NotImplementedException();
    }

    public int CompareTo(IBonkers other)
    {
        throw new NotImplementedException();
    }

    public bool Equals(IBonkers other)
    {
        throw new NotImplementedException();
    }
}