C#成员变量初始化; 最佳实践?

110

在声明时初始化类成员变量是否更好?

private List<Thing> _things = new List<Thing>();
private int _arb = 99;

或者在默认构造函数中?

private List<Thing> _things;
private int _arb;

public TheClass()
{
  _things = new List<Thing>();
  _arb = 99;
}

这只是风格问题,还是有性能方面的权衡取舍?


2
可能是https://dev59.com/8nVD5IYBdhLWcg3wTJvF的重复问题,最佳实践:在构造函数中初始化类字段还是在声明时? - goodeye
7个回答

87

就性能而言,没有真正的区别;字段初始化器是作为构造函数逻辑实现的。唯一的区别是字段初始化器发生在任何“base”/“this”构造函数之前。

构造函数方法可与自动实现属性一起使用(字段初始化器不能)-即

[DefaultValue("")]
public string Foo {get;set;}
public Bar() { // ctor
  Foo = "";
}

除此之外,我倾向于使用字段初始化程序语法;我发现它可以使事情更加局部化。

private readonly List<SomeClass> items = new List<SomeClass>();
public List<SomeClass> Items {get {return items;}}

我不必四处搜寻以找到它的分配位置...

显然的例外情况是当您需要执行复杂逻辑或处理构造函数参数时,在这种情况下,基于构造函数的初始化是最好的选择。同样,如果您有多个构造函数,最好始终以相同的方式设置字段-因此您可以使用以下构造函数:

public Bar() : this("") {}
public Bar(string foo) {Foo = foo;}

编辑:作为一个旁注,请注意,在上面的例子中,如果还有其他带有字段初始化器的字段(未显示),则它们仅在调用base(...)的构造函数中直接初始化 - 也就是public Bar(string foo) 构造函数。另一个构造函数不运行字段初始化程序,因为它知道它们已经由this(...)构造函数完成。


我知道这是一个旧帖子,但我有一个问题:你所说的“调用base(...)的构造函数”是什么意思?你的public Bar(string foo) {Foo = foo;}似乎没有调用: base(),或者它是隐式发生的吗?感谢您的帮助。 - Bruno Santos
1
@Bruno 对于一个 class,每个构造函数都有一个隐式的 : base(),除非你添加更具体的内容 - 这可能是 : base(123, "abc"),也可能是 : this(123, "abc") - Marc Gravell
@Marc,那么初始化顺序是(1)字段初始化器(2)基类构造函数(3)本地构造函数吗? - prabhakaran

13

实际上,您展示的字段初始化器是一种方便的简写方式。编译器会将初始化代码复制到您为类型定义的每个实例构造函数的开头。

这有两个影响:首先,任何字段初始化代码在每个构造函数中都会重复出现;其次,您在构造函数中包含的任何代码以将字段初始化为特定值实际上都会重新分配这些字段。

因此,在性能和编译代码大小方面,将字段初始化程序移入构造函数中会更好。

另一方面,性能影响和代码“膨胀”通常可以忽略不计,并且字段初始化程序语法的重要好处是减少您可能会忘记在其中一个构造函数中初始化某些字段的风险。


2
性能点只适用于重新分配值的情况(即在原始代码中不适用)。同样,"膨胀"问题(微不足道)仅适用于调用base(...)的构造函数 - 因此您可以通过使用私有构造函数(如发布的)来避免这个问题 - 只有这个构造函数将初始化字段。 - Marc Gravell
你知道这个重新赋值在实践中是否会发生吗?编译器应该能够删除额外的初始化,如果它不改变程序语义。 - Jørgen Fogh

6
使用字段初始化器的一个主要限制是无法将它们包装在try-finally块中。如果在字段初始化器中抛出异常,则之前分配的任何资源都将被放弃,无法防止这种情况发生。如果构造中存在其他错误,则可以通过保护基础构造函数接受IDisposable并将其指向自身作为其第一个操作来解决,尽管有些笨拙。然后,可以避免直接调用构造函数,而是通过工厂方法进行调用,在异常情况下会在部分创建的对象上调用Dispose。如果主类构造函数在“偷运”新对象的引用后失败,则此保护将允许清理在派生类初始化器中创建的IDisposable。不幸的是,如果字段初始化器失败,则无法提供此类保护。

3

对于实例变量,这主要是一种风格问题(我更喜欢使用构造函数)。对于静态变量,有一个性能优势可以通过内联初始化来实现(当然,并非总是可行)。


3

使用字段初始化器或创建Init()函数。将这些内容放入构造函数中的问题在于,如果您需要添加第二个构造函数,则最终会出现复制/粘贴代码(或者您忽略它并最终得到未初始化的变量)。

我建议在声明时进行初始化。或者让构造函数调用一个Init()函数。


6
请注意Marc Gravell在上面的帖子中使用了:this()来链接构造函数。这比编写单独的Init()函数要好得多。 - Tor Haugen

1

这完全取决于你。
我经常在行内初始化它们,因为当我不真正需要构造函数时,我不喜欢有一个(我喜欢小类!)。


0

关于上述的一个额外要点——在实现具有实现的类时,您总是会有一个构造函数。如果您没有声明,则编译器会推断出默认值 [public Foo(){}]; 即不带参数的构造函数。

通常我喜欢提供两种方法。允许构造函数用于那些希望使用它们的情况,并允许字段初始化器用于希望使用类/类型的简化或默认实现的情况。这为您的代码增加了灵活性。请记住,任何人都可以选择使用默认字段初始化程序...如果您提供多个构造函数,请务必手动声明 - public Foo(){}


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