非只读的匿名类型替代方案

47
在C#中,匿名类型可以如下定义:
method doStuff(){
     var myVar = new {
         a = false, 
         b = true
     }

     if (myVar.a) 
     {
         // Do stuff             
     }
}

然而,以下内容将无法编译:

method doStuff(){
     var myVar = new {
         a = false, 
         b = true
     }

     if (myVar.a) 
     {
         myVar.b = true;
     }
}
这是因为myVar的字段是只读的,不能被赋值。似乎想要做类似后者的事情是相当常见的,也许我见过的最好的解决方案就是在方法外部定义一个struct。
不过,真的没有其他方法使上面的代码块工作吗?这让我困扰的原因是,myVar是该字段的局部变量,因此似乎应该仅在使用它的方法内引用它。此外,需要将结构体放在方法外部可能会使对象的声明与其使用相距甚远,特别是在长方法中。
换句话说,是否有另一种替代匿名类型的方法,允许我定义像这样的“结构体”(我意识到结构体存在于C#中,必须在方法外定义),而不使其变为只读?如果没有,那么是否在想要这样做时存在根本性错误,并且我应该使用不同的方法?

6
匿名类型设计用于一次性、快速使用。如果您的使用比这更复杂,则有充分理由创建一个命名类型。 - Oybek
7个回答

40
不行,你需要创建自己的类或结构体来实现这个(如果您想它是可变的话,最好使用类 - 可变的结构体很糟糕)。
如果您不关心Equals / ToString / GetHashCode的实现,那么很容易:
public class MyClass {
    public bool Foo { get; set; }
    public bool Bar { get; set; }
}

(出于各种原因,我仍然会使用属性而不是字段,)。

就个人而言,我通常希望有一个不可变的类型,可以在方法之间进行传递等操作——我想要现有匿名类型功能的命名版本...


2
我发现自己想要的是一个快速声明带有特定字段的结构体的简写方式,以及一个自动生成的构造函数,该构造函数接受这些字段作为参数。顺便提一下,如果结构体没有修改this或访问外部实体的属性的成员函数,则“不可变”结构体和“可变”结构体在任何不直接尝试修改其字段的代码中都无法区分。我可以理解反对尝试修改结构体字段而不知道自己在做什么的观点,但这并不意味着必须禁止对结构体进行此类修改。 - supercat
2
@supercat:我见过由可变结构体引起的足够多的问题,以至于除了最特殊的情况外,我都会避免使用它们。这样的情况确实存在,但我发现它们很少见。考虑到你的回答,我怀疑我们必须就可变结构体是否是一个好主意达成不同意见。 - Jon Skeet
1
有多少个问题没有涉及编写“this”的结构体方法?您能否指出任何(非编写“this”的)问题的描述,这些问题不是由于某人不理解结构体工作方式而产生的明显结果(例如从集合中获取某些内容,修改该内容,并轻率地期望它更新集合 - 这在类中可能有效,但在结构体中肯定无效)? - supercat
2
@supercat - 虽然您可能知道可变结构的所有可能陷阱,但您能保证每个维护或(如果是公共的)使用您代码的开发人员都会意识到这些问题吗?虽然我不相信编码应该迎合最低公共分母,但可变结构与我所喜欢的“成功之坑”截然相反。 - TrueWill
@supercat:在Stack Overflow上留意一下它们就好了。不仅仅是当事情编译后不能按预期工作时,而且还有人们期望能够修改某些情况,但实际上这并没有帮助到他们的时候... - Jon Skeet
显示剩余4条评论

27
有没有一种替代匿名类型的方法可以让我简洁地定义一个类似这样的简单“记录”类型,而不将其设置为只读?
没有。您必须创建一个命名类型。
如果没有,那么想做这件事情有什么根本性错误吗?
没有,这是一个我们曾考虑过的合理特性。
我注意到,在Visual Basic中,如果您想要的话,匿名类型是可变的。
关于可变匿名类型真正的“根本性错误”是,如果将其用作哈希键,则使用它们可能会很危险。我们使用的假设设计匿名类型是:(1)您将使用它们作为LINQ查询推导中等值连接操作的键,并且(2)在LINQ-to-Objects和其他实现中,连接将使用哈希表来实现。因此,匿名类型应该能够用作哈希键,而可变哈希键则具有危险性。
在Visual Basic中,GetHashCode实现不会使用匿名类型的可变字段中的任何信息。虽然这是一个合理的折衷方案,但我们认为在C#中,额外的复杂性不值得花费精力。

我之前没有意识到vb.net使用可变属性的类来实现匿名类型。这似乎是最糟糕的选择。它失去了所有从公开字段(只读或非只读)获得的性能优势,以及从可变结构(受到任何不能足够访问以替换实例为新实例的代码保护)或不可变类型获得的语义优势。 - supercat
@supercat:在VB中,它们是可选的可变的。如果您不喜欢这个功能,您可以选择不使用它。 - Eric Lippert
啊,我明白了。声明为“key”的属性是不可变的——只有那些没有声明的属性是可变的。我仍然不确定使用可变属性而不是字段来处理其余部分会带来什么好处。如果一个类或(可能的)派生类可能需要非默认行为,则成员需要成为属性而不是字段,但由于匿名类的可变成员始终使用默认行为,因此我不清楚将它们设置为属性会带来什么好处。 - supercat
1
@supercat:将它们设置为属性的一个合理原因是只有属性可以在WPF中绑定。因此,使用属性使匿名类成为WPF的数据源非常有用。虽然这可能不是语言设计者/实现者做出决定的原因,但它确实支持了该决定。 - Kevin Cathcart
@KevinCathcart:我还没有使用过WPF;它的想法是使用反射来尝试从附加的类实例中获取属性吗?WPF对可变性有什么期望?在异步访问的上下文中,匿名类型显然没有支持安全变异所需的基础设施。 - supercat
它确实使用反射。关于可变性,这取决于绑定模式。 "OneWay" 只需要属性可以被读取,"TwoWay" 需要读取和写入,而 "OneWayToSource" 只需要一个可写属性。匿名类不实现 IPropertyChangeNotifier,因此 WPF 在更改时不会自动通知,但根据使用情况,这可能不是问题,并且可以手动指示 WPF 重新绑定。在许多应用程序中,一旦数据源被绑定,所有进一步的访问都发生在 UI 线程上,从而避免了并发访问问题。 - Kevin Cathcart

16

在C# 7中,我们可以利用命名元组来完成这个技巧:

(bool a, bool b) myVar = (false, true);

if (myVar.a)
{
    myVar.b = true;
}

我本来希望能在C# 6中使用这个功能,但对于我的当前项目来说,我想我没那么幸运了...(除非我能说服一些人升级) - Zarepheth
这是个好主意,对我所需的功能完全可行。不过,在使用Visual Studio时要注意两个小细节。 1)ToString()只显示值(false, true),虽然你可以展开以查看成员。2)你不能重命名成员并让它自动传递更改。 - Simon_Weaver

4

您将无法获得漂亮的初始化语法,但.NET 4中引入的ExpandoObject类将作为可行解决方案。

dynamic eo = new ExpandoObject();

eo.SomeIntValue = 5;
eo.SomeIntValue = 10; // works fine

我认为ExpandoObject不支持命名、类型化、空值属性(因为它只是一个字典,所有的null值都是无类型的),而匿名类型则可以。 - MikeBeaton

3
对于上述类型的操作,您应该定义自己的可变结构体。可变结构体可能会给像Eric Lippert这样的编译器编写人员带来麻烦,并且在处理它们时.net存在一些不幸的限制,但是与类相比,可变“普通旧数据”结构体(所有字段都是公共的,在其中写入this的唯一公共函数是构造函数,或者仅从构造函数调用)的语义提供了更清晰的语义。
例如,考虑以下内容:
struct Foo { public int bar; ...other stuff; } int test(Action<Foo[]> proc1, Action<Foo> proc2) { foo myFoos[] = new Foo[100]; proc1(myFoos); myFoos[4].bar = 9; proc2(myFoos[4]); // Pass-by-value return myFoos[4].bar; }
假设没有不安全的代码,并且传入的委托可以被调用并且在有限时间内返回,那么test()将返回什么?Foo是一个具有公共字段bar的结构体就足以回答这个问题:无论Foo的声明中出现了什么,以及传递给proc1proc2的函数是什么,它都将返回9。如果Foo是一个类,则必须检查每个存在或将来存在的Action<Foo[]>Action<Foo>,才能知道test()将返回什么。确定Foo是一个具有公共字段bar的结构体似乎比检查可能被传递的所有过去和未来函数要容易得多。
在.net中,修改this的结构体方法处理得特别差,因此,如果需要使用方法来修改结构体,则几乎肯定最好使用以下这些模式之一:
myStruct = myStruct.ModifiedInSomeFashion(...); // 方法1 myStructType.ModifyInSomeFashion(ref myStruct, ...); // 方法2
而不是模式:
myStruct.ModifyInSomeFashion(...);
只要使用上述结构体修改模式,可变结构体就具有比不可变结构体或不可变类更高效且更易读的代码,并且比可变类更少出现问题。对于表示值聚合的东西,没有超出其所包含的值的身份,可变类类型通常是最糟糕的表示形式。

“更清晰的语义”显然是因人而异的,但我不同意所有成员都是公共的结构体会满足许多初学者的期望。传递值的可变结构体的语义有许多微妙的陷阱,正如此响应中推荐的晦涩修改模式所示。公共字段也会破坏结构体的封装性,这是面向对象编程的主要优点之一。在这个问题上跟随Eric和其他语言设计师的建议-就是不要这样做。 - John Melville
@JohnMelville:具有公共字段的结构体本质上就像用胶带粘在一起的一堆存储位置。在需要使用胶带将一堆存储位置粘在一起的情况下,使用 BoSLSTwDT(结构体)的代码将比试图使用其他类型来模拟它们的代码更清晰。使结构体令人困惑的主要原因是语言试图假装它们是一种“对象”形式。 - supercat

0

我知道这是一个非常老的问题,但如何替换整个匿名对象:

if (myVar.a)
{
    myVar = new
    { a = false, b = true };
}

`


0

我发现很烦人的是你不能像在VB中那样将匿名属性设置为读/写 - 我经常想使用EF / LINQ投影从数据库返回数据,然后在C#中对数据进行一些不能在数据库中完成的处理。最简单的方法是迭代现有的匿名实例并在进行操作时更新属性。需要注意的是,尽管EF.Core这样做现在不是很糟糕,因为您可以在单个查询中混合使用数据库函数和.NET函数。

我的解决方法是使用反射,在某些情况下可能会受到责备和否决,但它确实有效;如果底层实现改变并且所有代码中断,则需要自行承担风险。

public static class AnonClassHelper {

    public static void SetField<T>(object anonClass, string fieldName, T value) {
        var field = anonClass.GetType().GetField($"<{fieldName}>i__Field", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

        field.SetValue(anonClass, value);
    }

}
// usage
AnonClassHelper.SetField(inst, nameof(inst.SomeField), newVal);

在处理字符串时我使用的另一种方法是将属性定义为StringBuilder类型,然后在获得匿名类型实例后,可以通过StringBuilder方法设置这些单独的属性。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接