如何在C#中强制使用工厂模式来创建结构体

5

我有一个C#结构体,我需要禁止调用它的无参数构造函数。

MyStruct a;
/// init a by members   // OK
MyStruct b = MyStruct.Fact(args); // OK, inits by memebers

MyStruct s = new MyStruct(); // can't have that

我这么做主要是为了强制所有成员都有明确的值,因为没有有效的默认值,而且所有成员必须有有效的值。

在C++中,这很容易,只需添加一个私有构造函数,但C#不允许这样做。

有没有一种方法可以防止上述情况发生?

我真的需要强制使用工厂,所以防止所有公共构造函数调用同样有效。


完整的披露:为了避免对mono的依赖,C#应用程序被自动转换为D语言,其中new Struct()会导致指针,并且这使我的事情变得混乱。然而,尽管如此,这个问题仍然是相关的,所以请忽略它。


我喜欢“公共约束器”的想法 - 也许可以作为某些不明罪行的惩罚? - xan
5个回答

14
你不能这样做。在C#中,所有结构类型默认都有公共的无参构造函数。(在CLR中,几乎没有任何一个结构类型拥有无参构造函数,但它们总是可以像拥有一样使用。) 参见此问题 了解为什么你不能在结构体上定义自己的无参构造函数(至少在C#中是这样的)。
实际上,如果你愿意用IL编写值类型,你就可以防止这种情况发生。我刚刚验证了一下,如果确保你的值类型只有一个无参构造函数,并将其设置为internal,则无法编写MyStruct ms = new MyStruct(); 但这并不能阻止:
MyStruct[] array = new MyStruct[1];
MyStruct ms = array[0];

这种方法绕过了新限制,但实际上并没有为您带来任何好处。这其实挺好的,因为在IL中搞来搞去很麻烦。

您确定您真的想首先编写一个结构体吗?那几乎永远不是一个好主意。


Crud.... & 是的,我需要值语义,而且由于我像爆米花一样将这些东西随意传递,并加上一些“额外”的约束条件,如果我切换到类,将会导致主要性能问题。 - BCS
你是否测量过性能,以确保它实际上会影响性能?尝试使用一个类,使其不可变以保持值类型语义。 - Jon Skeet
@BCS:D在内存分配方面真的很慢吗?在.NET中,如果需要的话,我创建数百万个对象也不会有任何问题。当然,使用类的一个好处是通常可以避免创建新对象 - 只需保留引用,除非需要进行变异。 - Jon Skeet
为了上下文,我预计创建的数据结构中大约有25-50%是这种类型。 - BCS
你在这里进行代码转换真的有很大的收益吗?我从来不喜欢用一种语言编写代码,然后再转换成另一种语言。通常情况下,我宁愿直接用目标语言编写。虽然这可能会更加痛苦,但可以避免这种问题。 - Jon Skeet
显示剩余5条评论

3

你不能这么做。

结构体中的所有值必须在构造函数中初始化,没有办法在构造函数之外进行初始化。

你到底想通过这样做实现什么?结构体是值类型,因此大多数操作都会得到一个“新”的结构体。很难对结构体强制执行工厂所使用的约束条件。


你可以在结构体的静态方法中实现,就像我的工厂方法一样。 - BCS
主要的限制是禁止默认初始化。我想强制代码明确提供所有的值。 - BCS

2
任何人只要有权限,都可以随时创建一个结构体而不需要调用构造函数。想象一下这样的情况:
如果你创建了一个有1000个元素的对象数组,它们都会被初始化为null,因此没有任何构造函数被调用。
对于结构体来说,不存在null的情况。如果你创建了一个有1000个DateTime对象的数组,它们都会被初始化为零,即等于DateTime.Min。运行时的设计者选择让你能够创建一个结构体数组而不需要调用构造函数N次——很多人都不会意识到这是一个性能问题。
尽管如此,你的工厂模式是个好主意。你是否可以创建一个接口并将其公开,但将结构体设置为私有或内部?这是你最接近的方案了。

0
你可以创建一个结构体来检测它是否处于默认初始化状态,然后在这种情况下执行适当的操作。我保留了工厂模式,但在常见的简单情况下,构造函数也可以作为一个足够的工厂。
这是很多样板代码。由于你使用D语言,你可能会想到和我一样的事情,“我希望C#有模板混合”。
示例:
using System;

namespace CrazyStruct
{
    public struct MyStruct
    {
        private readonly int _height;
        private readonly bool _init; // Will be 'false' using default(MyStruct).

        /// <summary>
        /// Height in centimeters.
        /// </summary>
        public int Height
        {
            get
            {
                if (!_init)
                {
                    // Alternatively, could have the preferred default value set here.
                    // _height = 200; // cm
                    // _heightInit = true;
                    throw new InvalidOperationException("Height has not been initialized.");
                }
                return _height;
            }
            // No set:  immutable-ish.
        }

        private MyStruct(int height)
        {
            _height = height;
            _init = true;
        }

        public static MyStruct Factory(int height)
        {
            return new MyStruct(height);
        }
    }

    static class Program
    {
        static void Main(string[] args)
        {
            MyStruct my = MyStruct.Factory(195);
            Console.WriteLine("My height is {0} cm.", my.Height);
            try
            {
                var array = new MyStruct[1];
                var ms = array[0];
                Console.WriteLine("My height is not {0} cm.", ms.Height);
            }
            catch (Exception ex)
            {
                Console.WriteLine("Caught the expected exception: {0}.", ex);
            }
            Console.ReadKey();
        }
    }
}

0
将其放入自己的程序集中,并将没有参数的MyStruct()设置为Internal(在VB中为Friend)。将Factory与MyStruct()放在同一个程序集中,但具有公共访问器。
现在,工厂可以访问无参数的MyStruct,但从程序集外部调用的任何内容都必须使用工厂。
编辑:我的错,我未考虑到这是一个结构体。你不能用结构体做到这一点,只能用类 - 在这种情况下,我的先前声明仍然成立。

你不能为值类型编写自己的无参数构造函数,因此也不能将其定义为 internal。 - Jon Skeet
...或者使用反射,但是是的,这是我能想到的最好的答案。 - Matt Cruikshank
Jon:是的,你说得对 - 我一下子就把它看成了类 - 是我的错误。你需要将结构体改为类。 - BenAlabaster

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接