为什么不能为.NET中的结构定义默认构造函数?


261

在.NET中,值类型(C#struct)不能具有没有参数的构造函数。根据这篇文章,这是CLI规范要求的。发生的情况是,对于每个值类型,都会创建一个默认的构造函数(由编译器生成?),该构造函数将所有成员初始化为零(或null)。

为什么不允许定义这样的默认构造函数?

琐碎的用途是有理数:

public struct Rational {
    private long numerator;
    private long denominator;

    public Rational(long num, long denom)
    { /* Todo: Find GCD etc. */ }

    public Rational(long num)
    {
        numerator = num;
        denominator = 1;
    }

    public Rational() // This is not allowed
    {
        numerator = 0;
        denominator = 1;
    }
}

使用当前版本的C#,默认的Rational 0/0不是那么酷。

PS:默认参数是否可以帮助解决C#4.0的问题,还是将调用CLR定义的默认构造函数?


乔恩·斯凯特(Jon Skeet)回答:

以您的示例为例,当某人这样做时,您希望发生什么:

 Rational[] fractions = new Rational[1000];

它应该在构造函数中运行1000次吗?

当然应该,这就是为什么我首先编写默认构造函数的原因。当未定义显式默认构造函数时,CLR应该使用默认清零构造函数;这样,您只需为使用的商品付费。然后,如果我想要一个1000个非默认值Rational的容器(并且想优化掉1000个结构),我将使用List<Rational>而不是一个数组。

我认为,此原因不足以阻止定义默认构造函数。


3
+1曾经有过类似的问题,最终将结构转换为类。
Dirk Vollmar,

4
C#4中的默认参数无济于事,因为Rational()调用无参数ctor而不是Rational(long num=0, long denom=1)
LaTeX

6
请注意,在Visual Studio 2015随附的C#6.0中,将允许为结构编写零参数实例构造函数。因此new Rational()将调用构造函数(如果存在),但是如果不存在,new Rational()将等效于default(Rational)。无论如何,都鼓励default(Rational)您在想要结构的“零值”(建议的设计为“坏”数字Rational)时使用语法。值类型的默认值T始终为default(T)。因此new Rational[1000]永远不会调用struct构造函数。
杰普·斯蒂格·尼尔森

6
要解决此特定问题,您可以将其存储denominator - 1在结构体中,以便默认值变为0/1
miniBill 2015年

3
Then if I want a container of 1000 non-default Rationals (and want to optimize away the 1000 constructions) I will use a List<Rational> rather than an array.您为什么期望数组调用与结构的List不同的构造函数?
mjwills 17-10-18

Answers:


197

注意:下面的答案是在C#6之前写的很久的,它计划引入在结构中声明无参数构造函数的功能-但仍然不会在所有情况下都调用它们(例如,用于数组创建) (最后此功能未添加到C#6中)。


编辑:由于Grauenwolf对CLR的了解,我已经编辑了以下答案。

CLR允许值类型具有无参数的构造函数,但C#不允许。我相信这是因为它会带来一种期望,即在不调用构造函数时会调用它。例如,考虑一下:

MyStruct[] foo = new MyStruct[1000];

仅通过分配适当的内存并将其全部清零,CLR就能非常有效地执行此操作。如果必须将MyStruct构造函数运行1000次,效率将大大降低。(事实上,它没有-如果你这样做有一个参数的构造函数,当你创建一个数组没有得到执行,或者当你有一个未初始化的实例变量。)

C#中的基本规则是“任何类型的默认值都不能依赖任何初始化”。现在,他们可以允许定义无参数的构造函数,但随后又不需要在所有情况下都执行该构造函数-但这会导致更多的混乱。(或者至少,所以我相信论点是正确的。)

编辑:使用您的示例,当有人这样做时,您想发生什么:

Rational[] fractions = new Rational[1000];

它应该在构造函数中运行1000次吗?

  • 否则,我们将得出1000个无效的理智
  • 如果确实如此,那么如果我们要用实数值填充数组,那么我们可能会浪费大量的工作。

编辑:(回答了更多的问题)无参数构造函数不是由编译器创建的。就CLR而言,值类型不必具有构造函数-尽管事实证明,如果用IL编写它,它就可以。当您new Guid()在C#中编写“ ”时,将发出与调用常规构造函数时不同的IL。有关方面的更多信息,请参见此SO问题

怀疑在无参数构造函数的框架中没有任何值类型。毫无疑问,NDepend可以告诉我是否足够好……C#禁止它的事实足以让我认为这可能是一个坏主意。


9
简短的解释:在C ++中,结构和类只是同一枚硬币的两个方面。唯一的实际区别是,默认情况下一个是公开的,另一个是私有的。在.Net中,结构和类之间的区别要大得多,理解它很重要。
Joel Coehoorn

38
@Joel:虽然这并不能真正解释这个特殊限制,是吗?
乔恩·斯基特

6
CLR确实允许值类型具有无参数的构造函数。是的,它将为数组中的每个元素运行它。C#认为这是个坏主意,不允许这样做,但是您可以编写一个.NET语言来实现。
乔纳森·艾伦,

2
抱歉,我对以下内容感到困惑。Rational[] fractions = new Rational[1000];如果Rational是类而不是结构,是否还会浪费工作量?如果是这样,为什么类具有默认的ctor?
亲吻我的腋窝

5
@ FifaEarthCup2014:您必须更具体地说明“浪费大量工作”的含义。但是,无论哪种方式,都不会调用构造函数1000次。如果Rational是类,则最终将得到1000个空引用的数组。
乔恩·斯基特

48

struct是一种值类型,并且值类型在声明后必须立即具有默认值。

MyClass m;
MyStruct m2;

如果您如上所述声明两个字段而未实例化任何一个,则中断调试器,m将为null但m2不会。鉴于此,无参数的构造函数将毫无意义,实际上,结构上的所有构造函数所做的所有工作都是分配值,仅通过声明它本身就已经存在。实际上,可以在上面的示例中非常愉快地使用m2,并且可以调用m2的方法,并且可以操纵其字段和属性!


3
不知道为什么有人拒绝了你。您似乎是这里最正确的答案。
pipTheGeek

12
C ++中的行为是,如果类型具有默认构造函数,则在没有显式构造函数的情况下创建此类对象时将使用该构造函数。这可能已在C#中用于使用默认构造函数初始化m2,这就是为什么此答案无济于事的原因。
Motti

3
onester:如果您不希望结构体在声明时调用其自己的构造函数,则不要定义这样的默认构造函数!:)这就是Motti的话
Stefan Monov 2010年

8
@Tarik。我不同意。相反,无参数的构造函数将具有充分的意义:如果我想创建一个始终具有一个单位矩阵作为默认值的“矩阵”结构,您怎么能通过其他方式做到这一点?
Elo

1
不知道我完全同意“的确M2可以很高兴地使用。”。在先前的C#中可能确实如此,但是声明一个结构而不是newIt,然后尝试使用其成员是一个编译器错误
Caius Jard

18

尽管CLR允许,但C#不允许结构具有默认的无参数构造函数。原因是,对于值类型,编译器默认情况下既不会生成默认构造函数,也不会生成对默认构造函数的调用。因此,即使您碰巧定义了默认构造函数,也不会调用它,这只会使您感到困惑。

为避免此类问题,C#编译器不允许用户定义默认构造函数。并且由于它不会生成默认构造函数,因此在定义字段时无法初始化它们。

或最大的原因是结构是值类型,并且值类型由默认值初始化,并且构造函数用于初始化。

您不必使用new关键字实例化结构。相反,它像int一样工作;您可以直接访问它。

结构不能包含显式的无参数构造函数。结构成员将自动初始化为其默认值。

结构的默认(无参数)构造函数可以设置与全零状态不同的值,这将是意外行为。因此,.NET运行时禁止结构的默认构造函数。


到目前为止,这个答案是最好的。最后,限制的全部目的是避免出现意外情况,例如MyStruct s;不调用您提供的默认构造函数。
2015年

1
谢谢你的解释。因此,仅是缺少编译器就需要加以改进,从理论上讲,没有充分的理由禁止无参数的构造函数(只要它们只能被限制为仅访问属性)。
Elo

16

您可以创建一个静态属性来初始化并返回默认的“有理数”:

public static Rational One => new Rational(0, 1); 

并像这样使用它:

var rat = Rational.One;

24
在这种情况下,Rational.Zero可能会有些混乱。
凯文(Kevin)

13

简短说明:

在C ++中,结构和类只是同一枚硬币的两个方面。唯一的实际区别是,默认情况下一个是公开的,另一个是私有的。

.NET中,结构和类之间的区别要大得多。最主要的是struct提供了值类型的语义,而class提供了引用类型的语义。当您开始考虑此更改的含义时,其他更改也开始变得更有意义,包括您描述的构造函数行为。


7
您必须更加明确地了解值与引用类型拆分如何隐含这一点,我无法理解……
Motti

值类型具有默认值-即使您未定义构造函数,它们也不为null。乍一看,这并不排除还定义默认构造函数,但是框架内部使用此功能对结构进行了某些假设。
Joel Coehoorn

@annakata:在涉及反射的某些情况下,其他构造函数可能很有用。同样,如果对泛型进行了增强以允许使用参数化的“新”约束,则具有可以符合它们的结构将很有用。
supercat 2012年

@annakata我相信这是因为C#有一个特别强的要求,new实际上必须编写该文件才能调用构造函数。在C ++中,构造函数是在数组的声明或实例化时以隐藏方式调用的。在C#中,任何东西都是指针,所以从null开始,或者它是一个结构,并且必须从某种东西开始,但是当您不能编写new...(例如array init)时,这将破坏一个强大的C#规则。
v.oddou

3

我还没有看到相当于我要给出的最新解决方案的信息,所以就在这里。

使用偏移量将值从默认0移动到所需的任何值。在这里,必须使用属性,而不是直接访问字段。(也许使用c#7可能的功能,您可以更好地定义属性范围的字段,以使它们免受代码中的直接访问。)

此解决方案适用于仅具有值类型(无引用类型或可为空的结构)的简单结构。

public struct Tempo
{
    const double DefaultBpm = 120;
    private double _bpm; // this field must not be modified other than with its property.

    public double BeatsPerMinute
    {
        get => _bpm + DefaultBpm;
        set => _bpm = value - DefaultBpm;
    }
}

这个答案,这种做法是不是特殊的外壳,但其使用的偏移这将为所有范围工作。

枚举作为字段的示例。

public struct Difficaulty
{
    Easy,
    Medium,
    Hard
}

public struct Level
{
    const Difficaulty DefaultLevel = Difficaulty.Medium;
    private Difficaulty _level; // this field must not be modified other than with its property.

    public Difficaulty Difficaulty
    {
        get => _level + DefaultLevel;
        set => _level = value - DefaultLevel;
    }
}

就像我说的那样,即使struct仅具有value字段,此技巧可能也并非在所有情况下都有效,只有您知道它是否适用于您的情况。只是检查。但是你有大致的想法。


对于我给出的示例,这是一个很好的解决方案,但实际上仅是一个示例,问题很笼统。
Motti

2

只是特殊情况而已。如果看到分子0和分母0,则假装它具有您真正想要的值。


5
我个人不希望我的班级/结构有这种行为。默默地失败(或以开发人员认为最适合您的方式进行恢复)是通往未捕获错误的道路。
鲍里斯·卡伦斯

2
+1这是一个很好的答案,因为对于值类型,您必须考虑其默认值。这使您可以“设置”默认值及其行为。
IllidanS4希望莫妮卡回到2015年

这正是他们实现类Nullable<T>(例如int?)的方式。
乔纳森·艾伦

那是一个非常糟糕的主意。0/0应该始终是无效分数(NaN)。如果有人称new Rational(x,y)x和y恰好是0怎么办?
Mike Rosoft

如果您有实际的构造函数,则可以引发异常,从而防止发生真正的0/0。或者,如果您确实希望发生这种情况,则必须添加一个额外的布尔值来区分default和0/0。
乔纳森·艾伦,


1

您无法定义默认构造函数,因为您正在使用C#。

结构可以在.NET中具有默认构造函数,尽管我不知道支持它的任何特定语言。


在C#中,类和结构在语义上是不同的。结构是值类型,而类是引用类型。
汤姆·萨杜

-1

这是我对没有默认构造函数困境的解决方案。我知道这是一个较晚的解决方案,但是我认为值得注意的是这是一个解决方案。

public struct Point2D {
    public static Point2D NULL = new Point2D(-1,-1);
    private int[] Data;

    public int X {
        get {
            return this.Data[ 0 ];
        }
        set {
            try {
                this.Data[ 0 ] = value;
            } catch( Exception ) {
                this.Data = new int[ 2 ];
            } finally {
                this.Data[ 0 ] = value;
            }
        }
    }

    public int Z {
        get {
            return this.Data[ 1 ];
        }
        set {
            try {
                this.Data[ 1 ] = value;
            } catch( Exception ) {
                this.Data = new int[ 2 ];
            } finally {
                this.Data[ 1 ] = value;
            }
        }
    }

    public Point2D( int x , int z ) {
        this.Data = new int[ 2 ] { x , z };
    }

    public static Point2D operator +( Point2D A , Point2D B ) {
        return new Point2D( A.X + B.X , A.Z + B.Z );
    }

    public static Point2D operator -( Point2D A , Point2D B ) {
        return new Point2D( A.X - B.X , A.Z - B.Z );
    }

    public static Point2D operator *( Point2D A , int B ) {
        return new Point2D( B * A.X , B * A.Z );
    }

    public static Point2D operator *( int A , Point2D B ) {
        return new Point2D( A * B.Z , A * B.Z );
    }

    public override string ToString() {
        return string.Format( "({0},{1})" , this.X , this.Z );
    }
}

忽略了我有一个名为null的静态结构的事实,(请注意:这仅适用于所有正象限),使用get; set;即可。在C#中,您可以进行try / catch / finally,以处理默认构造函数Point2D()未初始化特定数据类型的错误。我想这对于某些人来说是一个难以捉摸的解决方案。那就是为什么我要加我的。在C#中使用getter和setter功能将使您可以无视此默认构造函数,并尝试捕获尚未初始化的内容。对我来说,这很好用,对于其他人,您可能想添加一些if语句。因此,在需要分子/分母设置的情况下,此代码可能会有所帮助。我只想重申一下,该解决方案看起来并不好,从效率的角度来看可能效果更差,但是,对于来自较旧版本C#的用户,使用数组数据类型可为您提供此功能。如果您只想要一些可行的方法,请尝试以下操作:

public struct Rational {
    private long[] Data;

    public long Numerator {
        get {
            try {
                return this.Data[ 0 ];
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                return this.Data[ 0 ];
            }
        }
        set {
            try {
                this.Data[ 0 ] = value;
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                this.Data[ 0 ] = value;
            }
        }
    }

    public long Denominator {
        get {
            try {
                return this.Data[ 1 ];
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                return this.Data[ 1 ];
            }
        }
        set {
            try {
                this.Data[ 1 ] = value;
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                this.Data[ 1 ] = value;
            }
        }
    }

    public Rational( long num , long denom ) {
        this.Data = new long[ 2 ] { num , denom };
        /* Todo: Find GCD etc. */
    }

    public Rational( long num ) {
        this.Data = new long[ 2 ] { num , 1 };
        this.Numerator = num;
        this.Denominator = 1;
    }
}

2
这是非常糟糕的代码。为什么在结构中有数组引用?为什么不简单地将X和Y坐标作为字段?使用异常进行流控制是一个坏主意。您通常应该以不会发生NullReferenceException的方式编写代码。如果确实需要此方法(尽管这样的结构更适合于类而不是结构),则应使用延迟初始化。(从技术上讲,您-完全不需要,除了第一个坐标的每个设置-每个坐标都设置两次。)
Mike Rosoft

-1
public struct Rational 
{
    private long numerator;
    private long denominator;

    public Rational(long num = 0, long denom = 1)   // This is allowed!!!
    {
        numerator   = num;
        denominator = denom;
    }
}

5
允许,但未指定参数时不使用ideone.com/xsLloQ
Motti,
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.