首页 文章

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

提问于
浏览
221

在.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> 而不是数组 .

在我看来,这个原因并不足以阻止默认构造函数的定义 .

10 回答

  • 175

    注意:下面的答案是在C#6之前编写的,它计划引入在结构中声明无参数构造函数的能力 - 但它们仍然不会在所有情况下被调用(例如,用于数组创建)(最后)这个功能was not added to C# 6) .


    编辑:由于格劳恩沃尔夫对CLR的洞察力,我编辑了下面的答案 .

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

    MyStruct[] foo = new MyStruct[1000];
    

    CLR能够通过分配适当的内存并将其归零来非常有效地完成此任务 . 如果它必须运行MyStruct构造函数1000次,那将效率低得多 . (事实上,它没有 - 如果你有一个无参数构造函数,它在创建数组时没有运行,或者你有一个未初始化的实例变量 . )

    C#中的基本规则是"the default value for any type can't rely on any initialization" . 现在他们可以允许定义无参数构造函数,但是不需要在所有情况下执行构造函数 - 但这会导致更多的混淆 . (或至少,所以我相信这个论点 . )

    编辑:要使用您的示例,当有人执行时您希望发生什么:

    Rational[] fractions = new Rational[1000];
    

    它应该通过你的构造函数1000次?

    • 如果没有,我们最终会得到1000个无效的理由

    • 如果确实如此,那么我们将重新填充具有实际值的数组 .

    编辑:(回答更多问题)就CLR而言,无参数构造函数不必具有构造函数 - 尽管事实证明如果你在IL中编写它也可以 . 当您在C#中编写“ new Guid() ”时,如果调用普通构造函数,则会发出不同的IL . 有关该方面的更多信息,请参阅this SO question .

    我怀疑这不是一个坏主意 .

  • 0

    结构是值类型,值类型一旦声明就必须具有默认值 .

    MyClass m;
    MyStruct m2;
    

    如果你声明上面的两个字段而没有实例化,那么打破调试器, m 将为null,但 m2 将不会 . 鉴于此,无参数构造函数没有任何意义,实际上结构上的所有构造函数都是赋值,事物本身只是通过声明它就已存在 . 实际上m2可以很高兴地用在上面的例子中并调用它的方法,如果有的话,它的字段和属性被操纵!

  • 14

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

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

    或者最重要的原因是结构是值类型和值类型由默认值初始化,构造函数用于初始化 .

    您不必使用 new 关键字实例化您的结构 . 它反过来像一个int;你可以直接访问它 .

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

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

  • 40

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

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

    并使用它像:

    var rat = Rational.One;
    
  • 1

    更简短的解释:

    在C中,struct和class只是同一枚硬币的两面 . 唯一真正的区别是,一个是默认公开,另一个是私人 .

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

  • 1

    只是特殊情况吧 . 如果你看到0的分子和0的分母,假装它具有你真正想要的值 .

  • 3

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

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

  • 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语句 . 因此,在您需要Numerator / Denominator设置的情况下,此代码可能会有所帮助 . 我只想重申,这个解决方案看起来不太好,从效率的角度来看可能效果更差,但是,对于来自旧版本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;
        }
    }
    
  • 14

    我没有看到相当于我要给的后期解决方案,所以在这里 .

    使用偏移将值从默认值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只有值字段,只有你知道它是否适用于你的情况 . 试试看 . 但你得到了一般的想法 .

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

相关问题