在 .NET 中,值类型 (C# struct
) 不能有没有参数的构造函数。根据 this post,这是 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>
而不是数组。
在我看来,这个原因不足以阻止定义默认构造函数。
Rational()
调用无参数 ctor 而不是 Rational(long num=0, long denom=1)
。
new Rational()
将调用它,但如果它不存在,则 new Rational()
将等效于 default(Rational)
。在任何情况下,当您想要结构的“零值”(对于您建议的 Rational
设计而言,这是一个“坏”数字)时,我们鼓励您使用语法 default(Rational)
。值类型 T
的默认值始终为 default(T)
。所以 new Rational[1000]
永远不会调用结构构造函数。
denominator - 1
存储在结构中,以便默认值变为 0/1
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 不同的构造函数?
注意:下面的答案是在 C# 6 之前很久写的,它计划引入在结构中声明无参数构造函数的能力 - 但它们仍然不会在所有情况下都被调用(例如用于创建数组)(最后是这个特性was not added to C# 6)。
编辑:由于 Grauenwolf 对 CLR 的深入了解,我编辑了下面的答案。
CLR 允许值类型具有无参数构造函数,但 C# 不允许。我相信这是因为它会引入一个期望,即构造函数会在它不会被调用时被调用。例如,考虑一下:
MyStruct[] foo = new MyStruct[1000];
只需分配适当的内存并将其全部归零,CLR 就能够非常有效地做到这一点。如果它必须运行 MyStruct 构造函数 1000 次,那么效率会低很多。 (事实上,它没有——如果你有一个无参数的构造函数,当你创建一个数组或者你有一个未初始化的实例变量时它不会运行。)
C# 中的基本规则是“任何类型的默认值都不能依赖于任何初始化”。现在他们可以允许定义无参数的构造函数,但不需要在所有情况下都执行该构造函数——但这会导致更多的混乱。 (或者至少,所以我相信这个论点。)
编辑:以您的示例为例,当有人这样做时,您希望发生什么:
Rational[] fractions = new Rational[1000];
它应该通过您的构造函数运行 1000 次吗?
如果不是,我们最终会得到 1000 个无效的有理数
如果是这样,那么如果我们要用实际值填充数组,我们可能会浪费大量工作。
编辑:(回答更多问题)无参数构造函数不是由编译器创建的。就 CLR 而言,值类型不必具有构造函数——尽管事实证明,如果你用 IL 编写它可以。当您在 C# 中编写“new Guid()
”时,它会发出与调用普通构造函数不同的 IL。有关这方面的更多信息,请参见 this SO question。
我怀疑框架中没有任何带有无参数构造函数的值类型。毫无疑问,如果我问得足够好,NDepend 可以告诉我...... C# 禁止它的事实足以让我认为这可能是一个坏主意。
结构是值类型,值类型在声明后必须具有默认值。
MyClass m;
MyStruct m2;
如果您像上面那样声明两个字段而不实例化任何一个,然后中断调试器,m
将为空,但 m2
不会。鉴于此,无参数构造函数将毫无意义,事实上,结构上的所有构造函数所做的都是赋值,事物本身已经存在,只是通过声明它。实际上,m2 可以很高兴地用于上面的示例,并且可以调用它的方法(如果有的话),并操纵它的字段和属性!
new
它是编译器错误,然后尝试使用它的成员
您可以创建一个静态属性来初始化并返回默认的“有理数”:
public static Rational One => new Rational(0, 1);
并像这样使用它:
var rat = Rational.One;
Rational.Zero
可能不那么令人困惑。
简短的解释:
在 C++ 中,struct 和 class 只是一枚硬币的两个方面。唯一真正的区别是默认情况下一个是公共的,另一个是私有的。
在 .NET 中,结构和类之间的区别要大得多。最主要的是 struct 提供值类型语义,而 class 提供引用类型语义。当您开始考虑此更改的含义时,其他更改也开始变得更有意义,包括您描述的构造函数行为。
new
才能调用构造函数。在 C++ 中,构造函数在数组的声明或实例化时以隐藏的方式调用。在 C# 中,要么一切都是指针,所以从 null 开始,要么它是一个结构,并且必须从某个东西开始,但是当你不能写 new
...(如数组 init)时,这会破坏一个强大的 C# 规则。
从 C# 10.0 开始,您可以:
default
或在数组中创建时被激活......哎呀。
我还没有看到相当于我要给出的后期解决方案,所以就在这里。
使用偏移量将值从默认 0 移动到您喜欢的任何值。这里必须使用属性而不是直接访问字段。 (也许使用可能的 c#7 功能,您可以更好地定义属性范围字段,以便它们受到保护,不会在代码中被直接访问。)
此解决方案适用于仅具有值类型(无 ref 类型或可为空的结构)的简单结构。
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;
}
}
这是不同的than这个答案,这种方法不是特殊的大小写,而是它使用的偏移量适用于所有范围。
以枚举作为字段的示例。
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 只有值字段,也只有您知道它是否适用于您的情况。只是检查。但你明白了。
只是特例而已。如果您看到分子为 0,分母为 0,请假装它具有您真正想要的值。
Nullable<T>
(例如 int?
)之类的类的方式。
new Rational(x,y)
怎么办?
我使用的是 null-coalescing operator (??) 与这样的支持字段相结合:
public struct SomeStruct {
private SomeRefType m_MyRefVariableBackingField;
public SomeRefType MyRefVariable {
get { return m_MyRefVariableBackingField ?? (m_MyRefVariableBackingField = new SomeRefType()); }
}
}
希望这可以帮助 ;)
注意:null coalescing assignment 目前是针对 C# 8.0 的功能提议。
您无法定义默认构造函数,因为您使用的是 C#。
结构可以在 .NET 中具有默认构造函数,但我不知道任何支持它的特定语言。
我为此找到了简单的解决方案:
struct Data
{
public int Point { get; set; }
public HazardMap Map { get; set; }
public Data Initialize()
{
Point = 1; //set anything you want as default
Map = new HazardMap();
return this;
}
}
在代码中只做:
Data input = new Data().Initialize();
这是我对无默认构造函数困境的解决方案。我知道这是一个较晚的解决方案,但我认为值得注意的是这是一个解决方案。
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;
}
}
public struct Rational
{
private long numerator;
private long denominator;
public Rational(long num = 0, long denom = 1) // This is allowed!!!
{
numerator = num;
denominator = denom;
}
}
Rational
是类而不是结构,Rational[] fractions = new Rational[1000];
是否也会浪费大量工作?如果是这样,为什么类有一个默认的ctor?Rational
是一个类,您将得到一个包含 1000 个空引用的数组。