有没有办法在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
};
// ...
实现这一目标的合理策略是什么?你过去用过什么?
答案 0 :(得分:8)
由于没有语法糖,我知道你必须要么:
至少这是我现在能想到的。
我不认为最后两个是个好主意,因为你带来了大型机器来解决一个非常容易的问题。
是的,当你有数以千计的数据结构时,你可能会重新考虑这个,但是如果你只有几个数据结构,我就不会使用它。
所以剩下的基本上是智能构造函数和类似的东西 - 这是一个如何做到的简单示例(注意你并不需要所有这些 - 选择) - 它基本上是错过使用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.
}
除了必须输入所有这些的开销之外,如果你正在做(教条式)测试驱动开发(你没有 ),那也一直困扰着我,你也想测试这些方法。
但是,使用像AutoFixture和SemanticComparison这样的工具,你可以使它有些声明。这是一个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();
}
}