使用初始化块是否有害?

13

嗨,我在C#中使用初始化块。

new Something { foo = 1, bar = 2 };

但是有人说这是不好的做法。

我不认为这是错误的,不是吗?


1
这不是错误,这是非常自然的。 - Michael Buen
7个回答

11

你需要思考一下你的类型是否应该是可变的。就我个人而言,我喜欢不可变类型-它们使得理解正在发生的事情更容易,验证也更容易(一旦调用了构造函数并验证了状态,你就知道它不会变成无效状态),对于并发编程也非常有用。

另一方面,对象初始化程序在允许使用可变类型的情况下确实很有用。例如,ProcessStartInfo 实际上被用作 Process 的建造者类型。这样写是很有用的:

var info = new ProcessStartInfo { 
    FileName = "notepad.exe",
    Arguments = "foo.txt",
    ErrorDialog = true
};
Process process = Process.Start(info);

事实上,你甚至可以内联执行所有操作,而不需要使用额外的变量。我的 Protocol Buffers 代码库使用了相同的模式:

Foo foo = new Foo.Builder { 
    FirstProperty = "first",
    SecondProperty = "second"
}.Build();

现在,创建者模式的一种替代方案是构造函数参数(可能通过工厂方法)。其历史缺点是,您需要不同的重载来设置不同的属性,并且如果多个参数具有相同的类型,则很难确定哪个是哪个。C# 4通过可选参数和命名参数使此过程更加简便。例如,如果您正在构建一个电子邮件类,您可以使用:

Email email = new Email(
    from: "skeet@pobox.com",
    to: "jon@example.com",
    subject: "Test email",
    body: textVariable
);

这种方式在清晰度方面有许多与对象初始化器相同的好处,但没有可变性的惩罚。上面的构造函数调用可能会省略一些可选参数,如附件和BCC列表。我认为这将证明是C# 4中最大的收益之一,对于那些喜欢不可变性但也喜欢对象初始化器清晰度的人来说。


2
有趣的一点是,SO语法高亮器认为“from”是一个关键字(可能是因为支持LINQ高亮显示)。 - Dan Bryant
那么,命名参数看起来是我曾经写过的代码的一个很好的替代品:Fool x = new Fool("John" /* First */, "Doe" /* Last */, 79 /* Age */);,它们可以帮助区分模糊的方法调用,当其中一个参数为null时,但是否存在使用命名参数会适得其反的情况呢? - Hamish Grubijan
@HamishGrubijan:嗯,如果含义已经很明显了,我就不会使用它们...而且这会使你的代码对参数重命名变得敏感。由于其他语言的原因,重命名参数一直是一个破坏性的改变,但命名参数将其更加广泛地打开了。 - Jon Skeet

8
这句话的意思是:“如果存在适当的构造函数重载,则使用初始化块作为替代是值得质疑的(我不会说“不好的”做法)。”
public class Entity
{
    public Entity()
    {
    }

    public Entity(int id, string name)
    {
        this.ID = id;
        this.Name = name;
    }

    public int ID { get; set; }
    public string Name { get; set; }
}

如果你有这个非常简单的类,那么通常最好写成这样:
var entity = new Entity(1, "Fred");

...比写作更容易:
var entity = new Entity { ID = 1, Name = "Fred" };

至少有两个很好的理由:
  1. 你不知道构造函数在做什么。在某些情况下,构造对象然后设置公共属性可能比通过构造函数本身传递值显著地增加了成本。(您可能知道这不是事实,但作为类的使用者,您不应该假定关心实现细节,因为它们可能会发生变化)。

  2. 如果其中一个或多个属性的名称更改或变为只读状态(第一个属性ID可能本来就应该是只读的,但由于ORM之类的架构限制而不是),您的代码不会出错。

然而,在一种情况下,您必须使用初始化器而不是重载的构造函数,那就是在Linq to SQL / EF查询中链接选择时:

var bars =
    from f in ctx.Foo
    select new Bar { X = f.X, Y = f.Y };
var bazzes =
    from b in bars
    select new Baz { ... };

这实际上可能会出现“无支持的映射”错误,如果您使用构造函数重载而不是默认构造函数+初始化程序。然而,这是所使用技术的限制(并且是一个不良的限制),而不是编码风格问题。
在其他情况下,您应该优先考虑构造函数重载而不是初始化程序。
如果没有有用/相关的构造函数重载可以执行与您的初始化程序相同的操作,则继续编写初始化程序,这没有任何问题。该功能存在有很好的原因-它使代码更容易编写和阅读。

这种情况经常发生吗?我记不清上一次看到复杂/昂贵的构造函数或核心属性发生重大变化(以至于我的编辑器无法轻松修复)。我经常阅读带有对象构造函数的代码,并想知道它们的参数含义。 - Ken
1
@Ken:WCF客户端(ClientBase<T>)是构造成本高的对象之一。我编写的许多类将在默认构造函数中执行默认初始化任务。虽然我不反对您关于可读性的观点,但也很重要意识到初始化器只是属性设置器的语法糖(即var x = new MyClass(); x.ID = 1; x.Name = "Fred";),这与向构造函数提供这些值的语义根本不同。 - Aaronaught

3

但是有人说这是不好的做法。

谁说的?至少,这是一个有争议的说法。

目前似乎非常流行,许多知名的C#博客都广泛使用它。

与使用构造函数相比,优点在于它更易读,因为代码清楚地显示了哪些属性被分配了什么值。比较一下:

var x = new Something { Foo = 1, Bar = 2 };

使用

var x = new Something(1, 2);

此外,如果没有适当的构造函数,代码比手动分配属性更加简洁:
var x = new Something();
x.Foo = 1;
x.Bar = 2;

就我个人而言,我更喜欢不可变对象(即一旦创建,就不能更改的对象)。不幸的是,初始化程序块不能与这种对象(目前)结合使用,因为要使此模式起作用,对象必须具有属性设置器,而不可变对象没有。

但只要所使用的对象不是不可变的,我认为没有什么强烈的理由反对使用初始化程序符号。


1
C# 4 允许构造函数调用看起来非常像对象初始化器 - 请参阅我的帖子以获取示例。 - Jon Skeet
@Jon 很高兴C# 4终于引入了这个功能。有趣的是,虽然VB中也有同样的功能,但很少被使用,尽管VB.NET从一开始就有它...我想知道为什么。 - Konrad Rudolph
@Konrad:如果您愿意,您可以更正您的名字。 :) http://meta.stackexchange.com/questions/45208/possible-feature-request-revert-a-display-name-change-within-24-hours - missingfaktor
@Rahul G:非常感谢。虽然我喜欢那个名字。;-) - Konrad Rudolph

2

初始化块是以下几个方面的良好实践:

  1. You get to create an object and override its properties before getting its reference

    this.ObjA = new ObjA
    {
        Age = 20,
        Name = "123",
    };
    
    // this.ObjA will not be set until properties have all been defined
    // - safer when multithreading
    
  2. The parameter-less constructor can still do things behind the scene (e.g. initialize state members).

  3. You can use in conjunction with constructors with parameters

    this.ObjA = new ObjA(20)
    {
        Name = "123",
    };
    
  4. Using parameter-less constructors is better for (de)serializing scenarios

    You can create various objects, change their state via GUI, serialize them, deserialize them elsewhere.

  5. This practice forces authors to write more robust code - where the order in which things are done is less lightly to cause the application to crash every time the class's metadata is changed.


2
另一方面,它强制类型是可变的...我肯定不会说它们是“很好”的做法。当您已经拥有可变状态时,它们非常有用,但我仍然更喜欢在构造函数中设置不可变状态。 - Jon Skeet
1
具有不可变状态的对象没有可设置的属性,因此初始化程序块不适用于它们。 - Danny Varod
对于第一点,为什么一个适当的构造函数不能提供同样的好处呢:this.ObjA = new ObjA(Age: 20, Name: "123"); - NetMage
@NetMage 因为 #4。当时没有参数的默认值(现在有了),所以你需要2^n个构造函数来覆盖所有初始化选项。 - Danny Varod

1

初始化块没有问题,但是如果您的类型例如有许多属性,而只有其中几个需要在每个实例上设置,则应该在构造函数中将它们设为必需。

您类的用户将知道他们无法创建对象而不指定这些值。


0

对于对象工作所必需的属性,应在构造函数中进行初始化,因此您应该在构造函数中提供适当的参数。

初始化块非常适用于C# 3.0的几个新功能,但请记住,它们并不是用来替换构造函数中的参数的。


-1

我认为这很不错。

因为它可以大大减少你的打字量。


1
实际上,使用适当重载的构造函数会减少打字量。然而,使用初始化器比使用构造函数更易读 - Konrad Rudolph
7
在编程中,打字速度几乎从不是限制因素。代码要易读,不要为了缩短敲击时间而牺牲可读性。 - Jon Skeet

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