【问题标题】:Would C# benefit from aggregate structs/classes? [closed]C# 会从聚合结构/类中受益吗? [关闭]
【发布时间】:2011-01-18 06:29:59
【问题描述】:

前言

tl;wr:这是一个讨论。

我知道这个“问题”更多的是讨论,因此我将其标记为社区 wiki。但是,根据How to Ask 页面,它可能属于这里,因为它专门与编程相关,经过一个小时的研究,在网络上的任何地方都没有讨论过,具体,与大多数 C# 程序员相关,并且是主题。此外,这个问题本身就是为了获得答案,不管我有什么偏见,我都会保持开放的态度:C# 真的会从聚合结构中受益吗?尽管有这个前言,我还是明白这一点被关闭,但如果有权限和意图关闭的用户将我重定向到网络上的适当讨论点,我将不胜感激。


简介


缺乏结构可变性

结构是 C# 中灵活但有争议的类型。它们提供堆栈分配的值类型组织范式,但不提供其他值类型的不变性。

有些人说结构应该代表值,并且值不会改变(例如int i = 5;,5 是不可变的),而有些人认为它们是带有子字段的 OOP 布局。

关于结构不变性(123)的争论,目前的解决方案似乎是让程序员强制执行不变性,但也没有解决。

例如,当结构作为引用(this page 的底部)访问时,C# 编译器将检测可能的数据丢失并限制分配。此外,由于结构构造函数、属性和函数能够执行任何操作,限制(对于构造函数)在返回控件之前分配所有字段,结构cannot be declared as constant,如果它们仅限于数据表示,这将是一个正确的声明.


结构、聚合的不可变子集

聚合 类 (Wikipedia) 是功能有限的严格数据结构,由于缺乏灵活性,注定要提供语法糖。在 C++ 中,它们“没有用户声明的构造函数,没有私有或受保护的非静态数据成员,没有基类,也没有虚函数”。尽管核心概念保持不变,但 C# 中此类类的理论细节在此有待商榷。

由于聚合结构严格来说是带有标记访问器的数据持有者,因此它们的不变性(在可能的 C# 上下文中)将得到保证。聚合也不能为空,除非指定了空运算符 (?),对于其他纯值类型。因此,许多非法的结构操作以及一些语法糖都成为可能。


用途


  1. 可以将聚合声明为 const,因为它们的构造函数将被强制执行,除了分配字段之外什么都不做。
  2. 聚合可用作方法参数的默认值。
  3. 聚合可以是隐式顺序的,便于与本地交互
  4. 聚合将是不可变的,不会对引用访问执行任何数据丢失。此类子字段修改的编译器检测可能会导致完整的、隐式的 reassignment.libraries。

假设语法


从 C++ 语法中,我们可以想象以下内容: (请记住,这是一个社区 wiki,欢迎和鼓励改进)

aggregate Size
{
    int Width;
    int Height;
}

aggregate Vector
{
    // Default values for constructor.
    double X = 0, Y = 0, Z = 0;
}

aggregate Color
{
    byte R, G, B, A = 255;
}

aggregate Bar
{
    int X;
    Qux Qux;
}

aggregate Qux
{
    int X, Y;
}

static class Foo
{
    // Constant is possible.
    const Size Big = new Size(200, 100);

    // Inline constructor.
    const Vector Gravity = { 0, -9.8, 0 };

    // Default value / labeled parameter.
    const Color Fuschia = { 255, 0, 255 };
    const Vector Up = { y: 1 };

    // Sub-aggregate initialization
    const Bar Test = { 20, { 4, 3 } };

    static void SetVelocity(Vector velocity = { 0, 1, 0 }) { ... }
    static void SetGravity(Vector gravity = Foo.Gravity) { ... }

    static void Main()
    {
        Vector v = { 1, 2, 3 };

        double y = v.Y; // Valid.

        v.Y = 5; // Invalid, immutable.
    }
}

隐式(重新)赋值

截至今天,在 C# 4.0 中分配结构的子字段是有效的:

Vector v = new Vector(1, 2, 3);
v.Z = 5; // Legal in current C#.

但是,有时,编译器可以检测结构何时被错误地作为引用访问,并禁止更改子字段。例如,(example question)

//(in a Windows.Forms context)
control.Size.Width = 20; // Illegal in current C#.

由于Size 是一个属性而struct Size 是一个值类型,我们将编辑实际属性的副本/克隆,在这种情况下这将毫无用处。作为 C# 用户,我们倾向于假设大多数东西都是通过引用来访问的,尤其是在 OOP 设计中,这会让我们认为这样的调用是合法的(如果 struct Sizeclass,它就是合法的)。

此外,在访问集合时,编译器还禁止我们修改struct子字段:(example question)

List<Vector> vectors = ... // Imagine populated data.
vectors[4].Y = 10; // Illegal in current C#.

关于这些不幸限制的好消息是,对于这种情况,编译器做了一半可能的聚合解决方案:检测它们何时发生。另一半是隐式地重新分配具有更改值的新聚合。

  • 在本地范围内,只需重新分配向量即可。
  • 在外部范围内时,找到一个 get,如果匹配的 set 访问器可访问,则重新分配给这个。

为此,为了避免混淆,委托必须标记为隐式:

implicit aggregate Vector { ... }
implicit aggregate Size { ... }


// Example 1
{
    Vector v = new Vector(1, 2, 3);
    v.Z = 5; // Legal with implicit aggregates.

    // What is implicitly done:
    v = new Vector(v.X, v.Y, 5); // Local variable, simply reassign.
}

// Example 2
{
    //(in a Windows.Forms context)
    control.Size.Width = 20; // Legal with implicit aggregates.

    // What is implicitly done:
    Size old = control.Size.__get(); // External, MSIL detects a get.
    // If MSIL can find a matching, accessible __set:
    control.Size.__set({ 20, old.Height });
}

// Example 3
{
    List<Vector> vectors = ... // Imagine populated data.
    vectors[4].Y = 10; // Legal with implicit aggregates.

    // What is implicitly done:
    Vector old = vectors[4].__get(); // External, MSIL detects a get.
    // If MSIL can find a matching, accessible __set:
    vectors[4].__set({ old.X, 10, old.Z });
}

// Example 4
{
    Vector The5thVector(List<Vector> vectors) { return vectors[4]; }
    ...
    List<Vector> vectors = ...;
    The5thVector(vectors).Y = 10; // Illegal with implicit aggregates.

    // This is illegal because the compiler cannot find an implicit
    // "set" to match. as it is a function return, not a property or
    // indexer.
}

当然,这最后的隐式重新赋值只是一种句法简化,可以或不能采用。我只是提出它,因为编译器似乎能够检测到对结构的这种引用访问,并且如果它是一个聚合,它可以很容易地为程序员转换代码。


总结

  • 聚合可以有字段;
  • 聚合是值类型;
  • 聚合是不可变的;
  • 聚合在堆栈上分配;
  • 聚合不能继承;
  • 聚合具有顺序布局;
  • 聚合具有顺序的默认构造函数;
  • 聚合不能有用户定义的构造函数;
  • 聚合可以有默认值和标签结构;
  • 可以内联定义聚合;
  • 聚合可以声明为常量;
  • 聚合可用作默认参数;
  • 除非指定,否则聚合不可为空 (?);

可能:

  • 聚合(可以)被隐式重新分配;请参阅 Marcelo Cantos 的回复和评论。
  • 聚合(可能)有接口;
  • 聚合(可能)有方法;

缺点

由于聚合不会取代结构,而是另一种组织方案,我找不到很多缺点,但希望 S/O 的 C# 资深人士能够填充这个 CW 部分。最后一点,请直接回答这个问题,并讨论它:C# 是否有利于聚合类,如本文所述?我无论如何都不是 C# 专家,而只是 C# 语言的爱好者,并且怀念这个对我来说似乎至关重要的功能。我正在向有经验的程序员寻求有关此案例的建议和 cmet。 我知道有许多变通方法存在并且每天都在积极使用它们,我只是认为它们太常见了,不容忽视。

【问题讨论】:

  • 如果我理解正确,您希望结构具有写时复制语义吗?
  • 这是一个假设聚合类的特征中的一个建议,但正如 Marcelo Cantos 指出的那样,它应该被忽略。
  • Wiki,然而,迁移到程序员可能是更好的选择。
  • 如果能够简单地通过列出其字段并让编译器为它自动生成一个构造函数来定义一个可变结构,那就太好了。不过,我可以看到让这样的声明产生除普通结构之外的任何东西的唯一优势是引入协方差的可能性(因此,即使 KeyValuePair 是可变 POD,也可以将 KeyValuePair&lt;String, Button&gt; 提供给期望KeyValuePair&lt;String, Control&gt;)。请注意,对于 POD,这种协方差始终是安全的...
  • ...因为传递一个拆箱的 POD 总是会复制数据,拆箱 POD 也是如此。传递盒装 POD 不会复制数据,但由于盒装 POD 不能在不先拆箱的情况下进行变异,因此任何人都无法获得与他们期望的确切类型不同的可变 POD(请注意,结构并非严格如此,因为它们可以实现非协变的接口)。另外,顺便说一句,通过在该字段中创建具有不同值的临时结构实例来更改公开结构中的字段,然后将临时结构复制到原始结构上是愚蠢的。

标签: c# class struct aggregate


【解决方案1】:

我希望首先使用您提出的语义定义结构。

但是,我们坚持现有的内容,我认为我们不太可能在 CLR 中获得全新的“类型”。引入一种新类型意味着将其引入每一种 .NET 语言,而不仅仅是 C#,这是一个很大的变化。

我认为更有可能——记住,当我谈论假设的、未宣布的未来产品的假设语言功能时,这些产品不存在也可能永远不会存在,我这样做只是为了娱乐目的——我们将找到一些方法来对类和结构进行更好的不变性注释和强制执行。无论所讨论的类型是值类型还是引用类型,编译器都可以更好地执行不可变性并使其更容易以不可变风格进行编程。如果编译器或 CLR 在编译时或 jit 时有更多已知的不变性保证,那么编译器或 CLR 还可以更好地优化在多核机器上运行的代码。

当您考虑您的提议时,您可能需要考虑一个有趣的问题:如果聚合类型有方法,“this”是 value 还是 variable?例如:

aggregate Vector
{
    int x, y, z;
    public void M(Action action)
    {
         Console.WriteLine(this.x);
         action();
         Console.WriteLine(this.x);
    }
}
...
Vector v = new Vector(1, 2, 3);
Action action = ()=>{ v = new Vector(4, 5, 6); };
v.M(action);

会发生什么? “this”是通过value传递给M的,在这种情况下,它会写出两次“1”,还是作为对变量的引用传递,其中如果观察到您所谓的“不可变”类型发生突变? (因为发生变异的是变量;根据定义,变量是允许变异的,这就是为什么它们被称为“可变的”。)

【讨论】:

  • 这是有争议的语义,虽然 1 在我看来应该写两次,因为动作重新分配了一个新值,一个新的不可变聚合。然后“this”确实会按值传递。
  • 我读这个答案的次数越多,我的大脑就越发狂。理论上这应该在运行时崩溃,不是吗?
  • @Lazlo:由于您对这些问题很感兴趣,您可能想阅读我关于值类型的各种怪癖和实现细节的文章:blogs.msdn.com/b/ericlippert/archive/tags/value+types
  • @Lazlo:C++ 不禁止结构上的方法。
  • @Lazlo:聚合中成员函数的唯一限制是用户声明的构造函数和虚拟成员函数。除此之外,成员函数允许在聚合中。
【解决方案2】:

这会做什么?

List<Vector> vectors = ...;
Vector v = vectors[4];
v.Y = 10;

还是这个?

Vector The5thVector(List<Vector> vectors) { return vectors[4]; }
...
List<Vector> vectors = ...;
The5thVector(vectors).Y = 10;

用隐式赋值替换诊断不会让你走得太远。可变结构如此成问题是有原因的,仅仅声明一个新概念聚合并不能解决任何这些问题。

最好的解决方案是首先在语言中禁止可变结构。第二个最佳解决方案是表现得好像它们被禁止一样。结构体应该是小而独立的,这消除了使它们不可变的任何缺点。

【讨论】:

  • 在第一种情况下,应应用示例 1。在第二种情况下,编译器应该阻止它,因为不能隐式确定反向“设置”。无论如何,没有可以隐式计算的反向访问,因为这是二级引用(C# 目前也不会阻止它)。
  • 好的,我现在可以看到你的目标了,它确实有一种逻辑,但我认为隐式赋值带来的混乱大大超过了好处。但是这种逻辑是基于可变结构已经受到折磨的语义,这是一个坏主意,IMO。如果你只是把它留在“聚合是具有方便初始化语法的不可变结构”,我想你会有所收获。
  • 可能。同样,在大多数编程场景中,为用户提供选项通常是最好的解决方案。考虑隐式赋值被阻塞,除非用户定义聚合“公共隐式聚合......”,在这种情况下,他必须知道并理解这种运算符的含义和对应物才能使用它。
  • 最好的解决方案是打破无数项目?我觉得很难相信
【解决方案3】:

不,它不会受益。无论如何,结构体作为可变类型更好。

首先...“隐式重新分配的不变性”实际上只是“低效的可变性”。

给定一个“Point”结构,如果你只打算改变 X 的值,为什么要强制重写整个内存结构呢?仅仅覆盖 X 比用新值覆盖 X 并用当前值覆盖 Y 更有效。这样的计划没有任何好处。

老实说,可变性的整个主题是一个视角问题。只有在将复杂对象作为一个整体引用时谈论可变性才有意义,并询问其各个部分是否会在保持对整个对象的引用的同时改变值。

例如,将字符串称为不可变字符串是有意义的,因为您将其称为表示字符集合的特定内存块,其中字符不会从任何引用的角度改变值它。另一方面,一个 int 结构是可变的,因为它的值可以通过一个简单的赋值来改变,任何对 int 结构的引用(指针)都会看到这些变化。

至于结构或聚合方法中的“this”,当然它应该始终引用结构/聚合在堆栈上的内存位置,因此通过匿名方法和委托更改结构值的更新应该被反映和看到作为可变的。总而言之,可变性在基本变量级别是一个好主意,而不变性最好在表示复杂对象并显式编码“不可变”行为的更高级别处理。

【讨论】:

  • 听听听听。真正需要的是一种有效的方法来实现具有类似于引用调用语义的结构属性。我希望看到实现的方式是将“SomeList[5].X=9”之类的语句转换为“int temp = 9; SomeList.ActOnElement(5, (ref指向它,参考 int 参数)=> {it.X = 参数;},临时)”。如果要对结构执行的操作对于单个 int 参数来说过于复杂,则编译器可以生成一个临时结构并传递它。有些场景需要...
  • ...不止一个 ref 参数,因此为了使这种风格发挥最佳效果,必须有一些方法来指定至少一些可变参数泛型函数的特殊情况。请注意,这种方法比简单地让属性返回对结构的引用要好,因为属性处理程序可以在被调用的例程对结构完成任何它想要的操作后采取一些行动。例如,如果 Bounds 的任何部分已更改,Control.Bounds 的访问器可以移动控件。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-10-16
  • 2011-03-04
  • 2013-07-20
  • 2021-05-28
  • 2011-10-14
  • 1970-01-01
相关资源
最近更新 更多