如何实现一个被封闭的、公共的嵌套类,只能由其封闭类创建?

3

目标

我的目标是实现一个封闭的、公共的嵌套类,只能由其封闭类创建 - 而不使用反射。

这意味着嵌套类不能有任何公共或内部构造函数或任何公共或内部静态工厂方法。

先前的工作

几年前的这篇文章似乎是答案。(整个线程都有关于我要实现的内容的大量信息。)

它的工作原理非常简单:它利用了嵌套类可以访问其封闭类的静态字段以及嵌套类的静态构造函数。

封闭类声明了一个静态的 Func<NestedClassType, NestedClassCtorArgType> 委托,该委托返回嵌套类的实例,以便封闭类可以将该委托用作工厂方法。

嵌套类本身有一个静态构造函数,它将封闭类的静态工厂委托初始化为一个委托,该委托将创建嵌套类的实例。

问题

不幸的是,我无法按照那个答案中的方式使其工作。原因是嵌套类的静态构造函数在封闭类使用工厂方法之前没有被调用,因此会出现空引用异常。(如果您查看本问题末尾的示例程序,就会明白我的意思。)

我的解决方法

我已经通过以下方式解决了这个问题:

  1. 向嵌套类添加一个内部静态的 Initialise() 方法,它什么也不做。
  2. 向封闭类添加一个静态构造函数,该函数调用嵌套类的 Initialise() 方法。

这很好用,但它留下了一个像 internal static void Initialise() 方法的小问题。

我的问题

有没有避免这种做法的方法?我不能不想从我上面链接的原始帖子中漏掉什么。我是否误解了答案?

有没有聪明的方法强制运行嵌套类的静态构造函数在我调用试图创建嵌套类实例的代码之前?

还有其他问题吗?

(我知道我可以为嵌套类编写一个公共接口,并返回它。这个问题不是要解决这种方法!)

示例代码

这是我的示例代码。尝试运行它,它将打印“Test”。然后尝试注释掉标记为<--- If you comment this out, things won't work的行并再次运行它。

using System;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main()
        {
            Outer outer = new Outer();
            Outer.Inner item = outer.Item("Test");
            Console.WriteLine(item.Text);
        }
    }

    public sealed class Outer
    {
        public Inner Item(string text)
        {
            return _nestedFactory(text);
        }

        // This static constructor calls the nested class's Initialise() method, which causes the
        // nested class's static constructor to run, which then sets the enclosing class's 
        // _nestedFactory field appropriately.

        static Outer()
        {
            Inner.Initialise(); // <--- If you comment this out, things won't work.
        }

        // This class has a private constructor.
        // I only want class Outer to be able to create instances of it.

        public sealed class Inner
        {
            private Inner(string value) // Secret private constructor!
            {
                text = value;
            }

            public string Text { get { return text; } }

            static Inner()
            {
                _nestedFactory = text => new Inner(text);
            }

            internal static void Initialise(){}
            readonly string text;
        }

        static Func<string, Inner> _nestedFactory;
    }
}

1
如果你愿意有一个 internal Initialise() 方法,为什么不愿意使用一个 internal 构造函数呢?这似乎是解决问题的更简单的方法... - Marc Gravell
1
@MarcGravell 从调用者的角度来看,内部初始化方法并不做任何事情 - 它不会创建任何东西。而内部构造函数确实会创建一些东西;我认为这两个东西是非常不同的。 - Matthew Watson
@Matthew 那我问一下:为什么不愿意在这里使用 internal - Marc Gravell
1
我很好奇需要这种类设计的场景是什么。你能详细说明一下吗? - Daniel Hilgarth
@MatthewWatson:感谢澄清。在我看来,你想要实现的目标并没有什么问题。我只是想问一下,是否有其他可以解决这个问题的方法。但如果你只是关注可见性,那也没什么不好的 :) - Daniel Hilgarth
显示剩余5条评论
3个回答

3
C#没有"friend"函数。一个实用的方法可能是简单地要求调用者证明他们是谁。一种方法是提供一个只有合法调用类才能知道的对象引用,即一个对外部类私有object。假设你不向外传递该对象,唯一规避此限制的方法是使用非公共反射。如果你需要保护防止非公共反射,那么这种情况就是无意义的,因为任何拥有访问非公共反射的权限的人都可以已经访问像private构造函数等东西。因此,可以使用以下方式:
class Outer {
    // don't pass this reference outside of Outer
    private static readonly object token = new object();

    public sealed class Inner {
        // .ctor demands proof of who the caller is
        internal Inner(object token) {
            if (token != Outer.token) {
                throw new InvalidOperationException(
                    "Seriously, don't do that! Or I'll tell!");
            }
            // ...
        } 
    }

    // the outer-class is allowed to create instances...
    private Inner Create() {
        return new Inner(token);
    }
}

谢谢。是的,我曾经是一名C++程序员,而friend关键字正是我用来解决这个特定问题的方法!不过,这个Token的想法也是一个相当不错的方法。它将其从编译时检测移动到运行时检测,这是一个轻微的缺点。 - Matthew Watson
@MatthewWatson确实,它的好处在于简单明了,即使对编译器也是如此-无需担心何时调用静态构造函数,这是一个非常棘手的问题,很难准确回答,并且实际上在.NET版本之间会发生变化(这可能会使旧的依赖于静态构造函数副作用的答案在.NET 4.5上无效)。至于运行时检测...是的,棘手。 - Marc Gravell

3

如果您需要强制运行一个类构造函数,但没有该类型的引用,则可以使用此代码:

static Outer()
{
    System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(typeof (Inner).TypeHandle);
}

那个方法确实可行。我只需要用你给我的那行代码替换Outer类静态构造函数中的Inner.Initialise()代码行。然后我就可以删除Inner类的Initialise()方法了。太好了。:) 这样显式地调用将确保我不依赖于任何未记录的静态类初始化顺序的更改。顺便说一下,这并不完全是学术性的 - 我现在正在真正的代码中使用它。 - Matthew Watson

1
我还使用了静态构造函数来严格控制嵌套类的可访问性,结果得到了类似过于复杂的代码。但最终我想出了一个简单明了的解决方案。基础是实现一个私有接口的显式实现。(没有静态构造函数)
    public sealed class Outer
    {
        private interface IInnerFactory
        {
            Inner CreateInner(string text);
        }

        private static IInnerFactory InnerFactory = new Inner.Factory();

        public Inner Item(string text)
        {
            return InnerFactory.CreateInner(text);
        }

        public sealed class Inner
        {
            public class Factory : IInnerFactory
            {
                Inner IInnerFactory.CreateInner(string text)
                {
                    return new Inner(text);
                }
            }

            private Inner(string value) 
            {
                text = value;
            }

            public string Text { get { return text; } }
            readonly string text;
        }
    }
}

这个解决方案还可以在编译时提供安全性。虽然Outer.Inner.Factory可以在outer之外实例化,但CreateInner方法只能通过IInnerFactory接口调用,这意味着只能在Outer中调用。 以下代码行在外部甚至在同一程序集中都无法编译(出现不同的错误):
    new Outer.Inner.Factory().CreateInner("");
    ((Outer.IInnerFactory)new Outer.Inner.Factory()).CreateInner("");

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