在构造函数或声明中初始化类字段?


413

我最近一直在用C#和Java进行编程,并且很好奇初始化我的类字段的最佳位置。

我应该在申报时这样做吗:

public class Dice
{
    private int topFace = 1;
    private Random myRand = new Random();

    public void Roll()
    {
       // ......
    }
}

还是在构造函数中?:

public class Dice
{
    private int topFace;
    private Random myRand;

    public Dice()
    {
        topFace = 1;
        myRand = new Random();
    }

    public void Roll()
    {
        // .....
    }
}

我真的很好奇你们中的一些资深人士认为这是最佳做法。我要保持一致并坚持一种方法。


3
请注意,对于结构,您不能具有实例字段初始化程序,因此您只能使用构造函数。
2014年

Answers:


310

我的规则:

  1. 不要在声明中的默认值初始化(nullfalse00.0...)。
  2. 如果您没有用于更改字段值的构造函数参数,则最好在声明中使用初始化。
  3. 如果字段的值由于构造函数参数而改变,则将初始化放入构造函数中。
  4. 在练习中保持一致(最重要的规则)。

4
我希望kokos意味着您不应该将成员初始化为其默认值(0,false,null等),因为编译器会为您做到这一点(1.)。但是,如果要将字段初始化为默认值以外的任何值,则应在声明(2.)中进行操作。我认为可能是因为使用“默认”一词使您感到困惑。
Ricky Helgesson,2012年

95
我不同意规则1-通过不指定默认值(无论它是否由编译器初始化)来让开发人员去猜测或去寻找有关特定语言默认值的文档。出于可读性考虑,我将始终指定默认值。
詹姆斯

32
类型的默认值default(T)始终是内部二进制表示形式为的值0
Olivier Jacot-Descombes 2013年

15
无论您是否喜欢规则1,它都不能与只读字段一起使用,必须在构造函数完成时对其进行显式初始化。
yoyo 2014年

36
我不同意那些不遵守规则1的人。可以期望其他人学习C#语言。正如我们不会在每个foreach循环中添加“对列表中的所有项目重复以下操作”的注释一样,我们不需要不断地重述C#的默认值。我们也不需要假装C#具有未初始化的语义。由于缺少值具有明确的含义,因此可以将其省略。如果理想的是明确的,则new在创建新的委托时应始终使用(如C#1所要求的)。但是谁呢?该语言是为认真的编码人员设计的。
爱德华·布雷

149

在C#中没关系。您提供的两个代码示例完全相同。在第一个示例中,C#编译器(或它是CLR?)将构造一个空的构造函数,并像在构造函数中一样初始化变量(Jon Skeet在下面的注释中对此有所解释)。如果已经有一个构造函数,那么任何“之上”的初始化都将移到它的顶部。

从最佳实践的角度来看,前者要比后者少出错,因为有人可以轻松地添加另一个构造函数而忘记链接它。


4
如果选择使用GetUninitializedObject初始化类,那是不正确的。ctor中的任何内容都不会被触碰,但将运行字段声明。
Wolf5 2013年

28
实际上,这确实很重要。如果基类构造函数调用在派生类中重写的虚拟方法(通常是个坏主意,但是可能会发生),则使用实例变量初始化程序,将在调用该方法时初始化该变量-而在该方法中使用初始化构造函数,他们不会。(实例变量初始化程序调用基类构造函数之前执行。)
Jon Skeet 2013年

2
真?我发誓我是通过C#(我认为是第二版)从Richter的CLR中获取此信息的,其要旨是这是语法糖(我可能看错了吗?),而CLR只是将变量塞入了构造函数中。但是您要说明的不是这种情况,即在疯狂的场景中调用成员初始化可以在ctor初始化之前触发,这是在基本ctor中调用虚拟并在相关类中进行覆盖的疯狂情况。我理解正确吗?您刚刚发现了吗?对于这个关于5岁帖子的最新评论感到困惑(OMG已经5年了?)。
Quibblesome

@Quibblesome:子类构造函数将包含对父构造函数的链接调用。语言编译器可以自由地在其之前包含尽可能多的代码,只要在所有代码路径上调用一次父构造函数,并且在该调用之前有限地使用正在构造的对象即可。我对C#的烦恼之一是,尽管它可以生成用于初始化字段的代码以及使用构造函数参数的代码,但它不提供基于构造函数参数初始化字段的机制。
supercat

1
@Quibblesome,如果已分配值并将其传递给WCF方法,则前一个问题将成为问题。这会将数据重置为模型的默认数据类型。最佳实践是在构造函数中进行初始化(后一种)。
Pranesh Janarthanan

16

C#的语义与Java略有不同。在C#中,在调用超类构造函数之前执行声明中的赋值。在Java中,此操作立即完成,然后允许使用“ this”(对于匿名内部类特别有用),这意味着两种形式的语义确实匹配。

如果可以,请将字段定为final。


15

我认为有一个警告。我曾经犯过这样一个错误:在派生类内部,我试图“在声明时初始化”从抽象基类继承的字段。结果是存在两套字段,一套是“ base”,另一套是新声明的字段,这花了我很多时间进行调试。

课程:初始化继承的字段,您可以在构造函数中进行。


因此,如果您用来引用该字段derivedObject.InheritedField,那么它是引用您的基础字段还是派生的字段?
RayLuo's

6

假设您的示例中为类型,则绝对希望在构造函数中初始化字段。例外情况是:

  • 静态类/方法中的字段
  • 键入为静态/最终/等的字段

我总是将类顶部的字段列表视为目录(此处包含的内容,而不是其使用方式),并将构造函数作为引言。方法当然是章节。


为什么是“绝对”呢?您仅提供了样式首选项,而没有说明其推理。哦,等等,没关系,根据@quibblesome的回答,它们“完全等效”,因此实际上只是个人风格的偏爱。
RayLuo's

4

如果我告诉你,那取决于什么?

我通常会初始化所有内容,并以一致的方式进行操作。是的,它过于明确,但维护起来也容易一些。

如果我们担心性能,那么我只初始化需要做的事情,然后将其放在最能带来回报的区域。

在实时系统中,我质疑我是否甚至需要变量或常量。

在C ++中,我经常在任何地方几乎都没有初始化,然后将其移动到Init()函数中。为什么?好吧,在C ++中,如果要初始化的对象在构造对象时可能引发异常,则可能会导致内存泄漏。


4

有很多各种各样的情况。

我只需要一个空清单

情况很明显。我只需要准备我的列表,并防止在有人向列表中添加项目时引发异常。

public class CsvFile
{
    private List<CsvRow> lines = new List<CsvRow>();

    public CsvFile()
    {
    }
}

我知道价值观

我完全知道默认情况下要具有的值,或者需要使用其他逻辑。

public class AdminTeam
{
    private List<string> usernames;

    public AdminTeam()
    {
         usernames = new List<string>() {"usernameA", "usernameB"};
    }
}

要么

public class AdminTeam
{
    private List<string> usernames;

    public AdminTeam()
    {
         usernames = GetDefaultUsers(2);
    }
}

空列表可能的值

有时我期望默认情况下有一个空列表,并可能通过另一个构造函数添加值。

public class AdminTeam
{
    private List<string> usernames = new List<string>();

    public AdminTeam()
    {
    }

    public AdminTeam(List<string> admins)
    {
         admins.ForEach(x => usernames.Add(x));
    }
}

3

在Java中,带有声明的初始化程序意味着始终使用相同的方式初始化字段,无论使用哪个构造函数(如果有多个)或构造函数的参数(如果有参数),尽管随后构造函数可能更改值(如果不是最终值)。因此,使用带有声明的初始化程序将向读者表明,无论使用哪种构造函数,也不管传递给任何构造函数的参数如何,初始化值都是该字段在所有情况下都具有的值。因此,仅在且始终在所有构造对象的值相同的情况下,才将带有声明的初始化程序使用。


3

C#的设计表明,最好使用内联初始化,否则就不会使用该语言。每当您可以避免在代码中不同位置之间进行交叉引用时,通常情况会更好。

还有静态字段初始化的一致性问题,静态字段初始化需要内联以获得最佳性能。构造函数设计的框架设计指南说:

✓考虑在内联而不是使用静态构造函数显式初始化静态字段,因为运行时能够优化没有显式定义的静态构造函数的类型的性能。

在这种情况下,“考虑”意味着这样做,除非有充分的理由不这样做。对于静态初始值设定项字段,一个很好的原因是初始化太复杂而无法内联编码。


2

保持一致很重要,但这是一个要问自己的问题:“我是否有其他构造函数?”

通常,我正在创建用于数据传输的模型,该类本身除了充当变量的存放区外不执行任何操作。

在这些情况下,我通常没有任何方法或构造函数。对于创建列表的排他性目的而言,创建一个构造函数对我来说很愚蠢,尤其是因为我可以在声明中内联初始化它们。

正如许多其他人所说,这取决于您的使用情况。保持简单,不要做多余的事情。


2

考虑一下您拥有多个构造函数的情况。不同的构造函数的初始化会有所不同吗?如果它们相同,那么为什么要对每个构造函数重复?这符合kokos语句,但可能与参数无关。例如,假设您想要保留一个标志,该标志显示如何创建对象。然后,不管构造器参数如何,对于不同的构造器,该标志将被不同地初始化。另一方面,如果对每个构造函数重复相同的初始化,则可能(无意中)在某些构造函数中更改初始化参数,而在其他构造函数中则没有更改。因此,这里的基本概念是,通用代码应具有相同的位置,并且不得在不同的位置重复。


1

在声明中设置值会有一点性能上的好处。如果在构造函数中设置它,则实际上它被设置了两次(首先设置为默认值,然后在ctor中重置)。


2
在C#中,始终始终将字段设置为默认值。初始化程序的存在没有区别。
杰弗里·惠特里奇

0

我通常尝试构造函数什么也不做,只是获取依赖关系并使用它们初始化相关的实例成员。如果您要对课程进行单元测试,这将使您的生活更轻松。

如果要分配给实例变量的值不受要传递给构造函数的任何参数的影响,则在声明时将其分配。


0

不是您的问题的直接答案 最佳实践的而是重要且相关的复习点是,对于通用类定义,要么将其保留在编译器中以使用默认值进行初始化,要么我们必须使用特殊的方法来初始化字段设置为默认值(如果这对于代码可读性而言是绝对必要的)。

class MyGeneric<T>
{
    T data;
    //T data = ""; // <-- ERROR
    //T data = 0; // <-- ERROR
    //T data = null; // <-- ERROR        

    public MyGeneric()
    {
        // All of the above errors would be errors here in constructor as well
    }
}

将通用字段初始化为其默认值的特殊方法如下:

class MyGeneric<T>
{
    T data = default(T);

    public MyGeneric()
    {           
        // The same method can be used here in constructor
    }
}

0

当您不需要逻辑或错误处理时:

  • 在声明时初始化类字段

当您需要一些逻辑或错误处理时:

  • 在构造函数中初始化类字段

当初始化值可用并且初始化可以放在一行上时,这很好地工作。但是,这种初始化形式由于其简单性而受到限制。如果初始化需要某种逻辑(例如,错误处理或用于填充复杂数组的for循环),则简单分配是不够的。实例变量可以在构造函数中初始化,可以使用错误处理或其他逻辑。

来自https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.