【问题标题】:How Does This List Assignment Work?此列表分配如何工作?
【发布时间】:2016-09-01 15:16:28
【问题描述】:

我看过这个代码示例,它看起来像是将一个数组初始化器分配给一个列表。我认为它不起作用,但它以某种方式编译。 {} 不是数组初始值设定项吗? Children 是 IList 类型。如果没有大括号前的“新列表”,它是如何工作的?

        var nameLayout = new StackLayout()
        {
            HorizontalOptions = LayoutOptions.StartAndExpand,
            Orientation = StackOrientation.Vertical,
            Children = { nameLabel, twitterLabel }
        };

编辑:当我尝试Children = new List<View>{ nameLabel, twitterLabel } 时,编译器会发出以下警告:“无法分配属性或索引器 Layout.Children,它是只读的。”

顺便说一句,代码片段来自 Xamarin:https://developer.xamarin.com/guides/xamarin-forms/getting-started/introduction-to-xamarin-forms/

【问题讨论】:

  • 前几天我也发现了这一点,我也无法确定它是如何工作的。
  • 根据MSDN docs on initializers,使用此初始化程序时,编译似乎重复调用Add。请注意,您不能直接在变量声明中使用此语法。例如IList<string> children = { "childfoo", "childbar"} 不会编译
  • @stephen.vakil:有时,你几乎可以。 string[] children = { "childfoo", "childbar"};

标签: c# xamarin


【解决方案1】:

这是集合初始化器的一个特例。

在 C# 中,数组初始值设定项花括号已被泛化为可与任何集合类构造函数一起使用。

如果任何类实现了System.Collections.IEnumerable 并且具有一个或多个Add() 方法,则它都支持这些。 Eric Lippert has a good post about this type of "pattern matching" in C#:编译器在这里所​​做的是他们所谓的“duck typing”,而不是传统的强类型 OOP,其中类的能力是基于继承和接口实现来识别的。 C# 在几个地方这样做。那篇文章中有很多我不知道的东西。

public class Foo : List<String>
{
    public void Add(int n)
    {
        base.Add(n.ToString());
    }
    public void Add(DateTime dt, double x)
    {
        base.Add($"{dt.ToShortDateString()} {x}");
    }
}

然后编译:

var f = new Foo { 0, 1, 2, "Zanzibar", { DateTime.Now, 3.7 } };

这是语法糖:

var f = new Foo();

f.Add(0);
f.Add(1);
f.Add(2)
f.Add("Zanzibar");
f.Add(DateTime.Now, 3.7);

你可以用这些玩一些非常奇怪的游戏。我不知道全力以赴是否是个好主意(实际上我确实知道——它不是),但你可以。我编写了一个命令行解析器类,您可以在其中通过集合初始化程序定义选项。它有十几个 Add 的重载,带有不同的参数列表,其中许多是通用的。编译器可以推断的任何东西都是公平的游戏。

同样,您可以将其从收益递减推到滥用功能的地步。

您看到的是相同初始化器语法的扩展,它允许您为类本身已经创建的不可分配成员执行集合初始化器:

public class Bar
{
    public Foo Foo { get; } = new Foo();
}

现在……

var b = new Bar { Foo = { 0, "Beringia" } };

{ 0, "Beringia" }Foo 实例的集合初始化器,Bar 为自己创建;这是语法糖:

var b = new Bar();

b.Foo.Add(0);
b.Foo.Add("Beringia");

当您以这种方式看待它时,编译器愿意在语法糖初始化程序用法中解决Foo.Add() 的重载是有道理的。我认为能够做到这一点很棒,但我对他们选择的语法并不是 100% 满意。如果您发现赋值运算符是一个红鲱鱼,其他人也会。

但我不是语法仲裁者,这可能是所有相关人员的最佳选择。

最后,这也适用于对象初始化器:

public class Baz
{
    public String Name { get; set; }
}

public class Bar
{
    public Foo Foo { get; } = new Foo { 1000 };
    public Baz Baz { get; } = new Baz { Name = "Initial name" };
}

所以...

var b = new Bar { Foo = { 0, "Beringia" }, Baz = { Name = "Arbitrary" } };

实际上变成了……

var b = new Bar();

b.Foo.Add(0);
b.Foo.Add("Beringia");
b.Baz.Name = "Arbitrary";

我们无法初始化Bar.Baz,因为它没有setter,但我们可以初始化它的属性,就像我们可以初始化Foo 中的项目一样。即使它们已经被附加到实际构造函数的不同对象初始化程序初始化,也是如此。

如您所料,集合初始化器是累积的:Bar.Foo 将包含三个项目:{ "1000", "0", "Beringia" }

当您将花括号视为一列赋值语句或 Add() 重载调用的简写时,这一切都会成为焦点。

但我同意在没有实际分配左值的情况下,等号是不和谐的。

奖金

这是我了解到的另一个模式匹配功能from that Eric Lippert article

public static class HoldMyBeerAndWatchThis
{
    public static IEnumerable<int> Select(Func<String, String> f)
    {
        yield return f("foo").Length;
    }
}

所以……

var x = from s in HoldMyBeerAndWatchThis select s;

select 工作所需要的只是你选择的东西必须有一个名为 Select 的方法,它返回像IEnumerable 中所述的东西嘎嘎声 EricLippert 在the linked article 中对foreach 的评论(感谢Eric!),并采用Func&lt;T,T&gt; 参数。

【讨论】:

  • Children 部分是一个集合初始化器 - 我认为这就是 OP 实际询问的内容。
  • 我真的不认为这是一回事。值得注意的是,Children = { nameLabel, twitterLabel } 似乎不包含构造函数调用。
  • 几件事。首先,C# 团队当然关心你发现什么是令人困惑或显而易见的;团队为这样的语法决策苦恼,尤其是当它们的细微之处不明显时。
  • 其次,在您的“选择”奖励情节中:它甚至不必返回 IEnumerable。它所要做的就是返回“遵循模式”的东西。 IEnumerable 遵循该模式,因为存在为 IEnumerable 提供该模式的扩展方法。应遵循的模式记录在 C# 规范中。
  • 我同意在对象初始化器的上下文中,PhoneNumbers = { "555-1212" }PhoneNumbers = new List&lt;string&gt; { "555-1212" } 之间存在差异,这有点微妙。但是我们需要处理这两种情况:集合已经存在并且属性是只读的,以及集合需要分配并分配给属性的情况。这是一个很难解决的问题,并且考虑了许多语法。
【解决方案2】:

其他两个答案似乎有些混淆,这实际上是如何工作的。我建议您参考关于对象初始化器的 C# 规范部分,它清楚地总结了语义:

在等号之后指定集合初始值设定项的成员初始值设定项是嵌入式集合的初始化。初始化器中给出的元素被添加到字段或属性引用的集合中,而不是为字段或属性分配新集合。

请记住,发布规范是为了您的方便;如果您对 C# 语言结构的含义有疑问,它(或印刷的注释版本“C# 编程语言”)应该是您的第一个参考。

【讨论】:

    【解决方案3】:

    并非总是如此。你的想法是:

    int[] array = new int[] { 1, 2, 3, 4 };
    

    这就是数组初始值设定项。但是,您也有:

    SomeObject obj = new SomeObject { Name = "Hi!", Text = "Some text!" };
    

    这是一个对象初始化器。您在“替换”损坏的代码中所拥有的完全是另一回事 - 集合初始化程序。这适用于实现 IEnumerable 并具有带有正确参数的 Add 方法的任何类型(在这种情况下,类似于 public void Add(View view))。

    SomeList list = new SomeList { "Hi!", "There!" };
    

    在您的情况下,使用后两个,并且集合初始化程序不会实例化新集合。一个简单的示例代码:

    void Main()
    {
      var some = new SomeObject { List = { "Hi!", "There!" } };
    
      some.List.Dump();
    }
    
    public class SomeObject
    {
      public List<string> List { get; private set; }
    
      public SomeObject()
      {
        List = new List<string>();
      }
    }
    

    在这种情况下,Main 中的代码大致转换为等效的 C# 代码:

    var some = new SomeObject();
    some.List.Add("Hi!");
    some.List.Add("There!");
    

    不过,此表单仅在对象初始化程序中有效 - 它专为您需要使用对象/集合初始化程序语法初始化的 readonly 字段的情况而设计。例如,这不起作用:

    var some = new SomeObject();
    some.List = { "Hi!", "There!" };
    

    如果编译器使用与以前相同的“技巧”,它只能将项目添加到集合中 - 但它不能保证集合是空的(尽管即使在对象初始化器的情况下,这也只是“由约定” - 如果您首先使用一些项目初始化列表,则在“分配”发生时它们将保留)。

    就是这样,伙计们:)

    【讨论】:

    • 据我所知,这是一个特殊规则,仅适用于对象初始化内部的集合初始化。有这方面的官方文件吗?
    • @JohnL。据我所知,最后一个 C# ECMA 规范还没有对象初始化器。我认为没有任何官方文档。
    • 当然有官方文档;此功能是十多年前实现的。请参阅过去十年中任何已发布的 C# 规范或 MSDN,或死树“C# 编程语言”。
    • @EricLippert 好吧,最新的 C# ECMA 规范 已经 有 10 年历史了。我在里面找不到提到这个。我在 MSDN 上也找不到。有对象初始化器、集合初始化器、数组初始化器……但我找不到关于这种特殊情况的任何信息。如果您有参考,我很乐意将其包括在内:)
    • 您无法找到的规范行是“在等号之后指定集合初始值设定项的成员初始值设定项是嵌入式集合的初始化。而不是将新集合分配给该字段或属性,初始化器中给定的元素将添加到字段或属性引用的集合中。"
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-12-23
    • 2011-03-26
    • 2023-04-10
    • 2018-11-16
    • 2013-02-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多