为什么我不能在.NET中定义一个结构体的默认构造函数?
在.NET中,值types(C# struct
)不能有一个没有参数的构造函数。 根据这篇文章,这是由CLI规范强制的。 会发生什么是每个值types的默认构造函数创build(由编译器?)所有成员初始化为零(或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>
而不是一个数组。
在我看来,这个原因不足以防止定义一个默认的构造函数。
注意:下面的答案是在C#6之前编写的,它计划引入在结构体中声明无参数构造函数的能力 – 但是在所有情况下它们仍然不会被调用(例如,用于数组创build) (最终此function未添加到C#6 )。
编辑:我已经编辑了下面的答案,因为格劳恩狼对CLR的洞察力。
CLR允许值types具有无参数的构造函数,但是C#没有。 我相信这是因为它会引入一个期望,即在不这样做的时候调用构造函数。 例如,考虑一下:
MyStruct[] foo = new MyStruct[1000];
CLR能够通过分配适当的内存并将其全部清零来非常高效地执行此操作。 如果它不得不运行MyStruct构造函数1000次,那效率就会低很多。 (事实上,它不是 – 如果你有一个无参数的构造函数,当你创build一个数组,或者当你有一个未初始化的实例variables时,它不会运行。
在C#中的基本规则是“任何types的默认值不能依赖任何初始化”。 现在,他们可以允许无参数的构造函数被定义,但是并不要求构造函数在所有情况下都被执行 – 但是这会导致更多的混淆。 (或者至less,所以我相信这个观点。)
编辑:使用你的例子,当有人做什么,你想要发生什么:
Rational[] fractions = new Rational[1000];
它应该通过你的构造函数1000次?
- 如果不是,我们最终会有1000个无效的理由
- 如果是这样,那么如果我们要用真正的值填充数组,我们可能会浪费大量的工作。
编辑:(更多的回答这个问题)无参数的构造函数不是由编译器创build的。 就CLR而言,值types不必具有构造函数 – 尽pipe事实certificate,如果使用IL编写它,也可以使用构造函数。 当你在C#中编写“ new Guid()
”时,如果你调用一个普通的构造函数,它会发出不同的IL。 看到这个问题多一点这方面。
我怀疑没有参数构造函数的框架中没有任何值types。 毫无疑问,NDepend可以告诉我,如果我很好地问它… C#禁止这一事实是一个足够的暗示,我认为这可能是一个坏主意。
结构是一个值types,值types一旦声明就必须有一个默认值。
MyClass m; MyStruct m2;
如果你如上所述声明两个字段而没有实例化,那么打破debugging器, m
将是空的,但m2
不会。 鉴于此,一个无参数的构造函数是没有意义的,事实上,一个结构体上的所有构造函数都是赋值的,事物本身已经存在,只要声明它。 事实上,在上面的例子中,m2可以相当愉快地使用,如果有的话,它的方法被调用,并且它的字段和属性被操纵!
您可以创build一个静态属性来初始化并返回一个默认的“合理”数字:
public static Rational One { get { return new Rational(0, 1); } }
并使用它:
var rat = Rational.One;
较短的解释:
在C ++中,struct和class只是同一枚硬币的两面。 唯一真正的区别是一个是默认公开的,另一个是私人的。
在.NET中 ,结构和类之间有更大的差别。 主要的是struct提供了值types的语义,而类提供了引用types的语义。 当你开始思考这个变化的含义时,其他变化也开始变得更有意义,包括你描述的构造器行为。
尽pipeCLR允许,但C#不允许结构体具有默认的无参数构造函数。 原因是,对于一个值types,编译器在默认情况下既不会生成默认构造函数,也不会生成对默认构造函数的调用。 所以,即使你碰巧定义了一个默认的构造函数,它也不会被调用,而这只会让你感到困惑。
为了避免这样的问题,C#编译器不允许用户定义默认的构造函数。 而且因为它不会生成默认构造函数,所以在定义它们时不能初始化字段。
或者,最大的原因是结构是一个值types,值types是通过默认值初始化的,而构造函数是用来初始化的。
您不必使用new
关键字来实例化您的结构。 它反而像一个int一样工作; 你可以直接访问它。
结构不能包含显式无参数的构造函数。 结构成员自动初始化为默认值。
结构的默认(无参数)构造函数可以设置不同于全零值状态的值,这将是意想不到的行为。 .NET运行时因此禁止一个结构的默认构造函数。
只是特殊情况而已。 如果你看到一个分子0和一个分母0,假装它有你真正想要的值。
你不能定义一个默认的构造函数,因为你正在使用C#。
.NET中的结构体可以有默认的构造函数,但是我不知道任何支持它的特定语言。
这是我的解决scheme,没有默认的构造函数的困境。 我知道这是一个晚期的解决scheme,但我认为值得注意的是这是一个解决scheme。
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( AX + BX , AZ + BZ ); } public static Point2D operator -( Point2D A , Point2D B ) { return new Point2D( AX - BX , AZ - BZ ); } public static Point2D operator *( Point2D A , int B ) { return new Point2D( B * AX , B * AZ ); } public static Point2D operator *( int A , Point2D B ) { return new Point2D( A * BZ , A * BZ ); } public override string ToString() { return string.Format( "({0},{1})" , this.X , this.Z ); } }
忽略事实,我有一个名为null的静态结构(注意:这仅适用于所有正象限),使用get; set; 在C#中,可以有一个try / catch / finally,用于处理特定数据types未由默认构造函数Point2D()初始化的错误。 我想这是难以解决的一些人在这个答案。 这主要是为什么我加我的。 在C#中使用getter和setterfunction将允许你绕过这个默认的构造函数,并试着抓住你没有初始化的东西。 对我来说,这工作正常,对于其他人,你可能想添加一些if语句。 所以,如果你想分子/分母设置,这个代码可能会有所帮助。 我只想重申,从效率的angular度来看,这个解决scheme看起来不太好,可能会更糟糕,但是对于来自旧版本C#的人来说,使用数组数据types可以提供这种function。 如果你只是想要的东西,可以试试这个:
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; } }
我还没有看到相当于我要给的晚期解决scheme,所以在这里。
使用偏移将值从默认值0移动到您喜欢的任何值。 这里必须使用属性而不是直接访问字段。 (也许有可能的C#7function,你最好定义属性范围的字段,使他们保持不被直接访问的代码。)
此解决scheme适用于只有值types的简单结构(无参考types或可空结构)。
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; } }
这不同于这个答案,这种方法不是特别的套pipe,但它使用的偏移量将适用于所有范围。
以枚举为字段的示例。
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只有值域,只有你知道它是否适用于你的情况。 只是检查。 但你得到了一般的想法。