【问题标题】:Do I use Nested Classes to prevent class instantiation or...?我是否使用嵌套类来防止类实例化或...?
【发布时间】:2011-10-30 01:43:24
【问题描述】:

我已经阅读了几篇关于何时使用嵌套类的文章,但没有一篇文章能解决我的具体问题。

C# 有一个名为 XmlReader 的类,它只提供一个 Create() 方法。我假设 create 创建了 XmlReader 的子类。如果不是,那么对于这个例子,假设它是。

考虑这种关系:

/// <summary>
/// Class to read information in a file on disk
/// </summary>
interface ILoad
{
  /// <summary> Version number of the file </summary>
  int Version {get;}

  /// <summary> Content of the file </summary>
  string Content {get;}

  /// <summary> Full path to the file </summary>
  string FullPath {get;}
}

/// <summary> Provides base loading functionality </summary>
class LoaderBase : ILoad
{
    public int Version {get; protected set;}
    public string Content {get; protected set;}
    public string FullPath{get; protected set;}

    /* Helpers omitted */
    protected abstract void Load(string pathToFile);

    public static LoaderBase Create(string pathToFile)
    {
      switch(Path.GetExtension(pathToFile))
      {
         // Select the correct loader based on the file extension and return
      }
      return null;//unknown file type
    }
}

/// <summary> Base class functionality to load compiled files </summary>
public abstract class CompiledLoaderBase : LoaderBase
{
  protected CompiledLoaderBase(string path)
  {
    Load(path);
  }

  protected override Load(string path)
  {
     /* read the file and create an XmlReader from it */
     ReadVersionNumber(reader);
     ReadContent(reader); 
  }

  protected abstract void ReadVersionNumber(XmlReader reader);
  protected abstract void ReadContent(XmlReader reader);

  // Wish I could call this Create, but inherited a static Create method already
  public static CompiledLoaderBase CreateCompiled(string path)
  {
     //Figure out which loader to create and return it
     // ... Assume we figured out we need V1
     return new CompiledLoaderV1(path);
  }


  // Here's the fun stuff!
  protected class CompiledLoaderV1 : CompiledLoaderBase
  {
    public CompiledLoaderV1(string path)
    : base(path)
    {}

    protected override ReadVersionNumber(XmlReader reader)
    { /* read the version number and store in Version */ }

    protected override ReadContent(XmlReader reader)
    { /* read the content and store in Content */ }
  }

  // ... More classes with their own methods for reading version and content

}

现在,我使用嵌套类来防止用户直接创建特定的加载器;他们必须使用抽象基的 Create* 方法之一。 FxCop 对此大发雷霆,我希望能弄清原因。

它提到不要使用嵌套类,而是使用命名空间。有没有办法通过命名空间来实现这一点?
编辑:具体来说,消息是:“NestedTypesShouldNotBeVisible”。解决方案:“不要嵌套类型 'CompiledLoaderBase+CompiledLoaderV1'。或者,更改其可访问性,使其在外部不可见。”信息:“不要使用公共、受保护或受保护的内部嵌套类型作为对类型进行分组的一种方式。为此目的使用命名空间。嵌套类型是最佳设计的场景非常有限。”现在,我相信 Jon Skeet 发现您无法通过命名空间来实现这一点。我只是想确认一下,因为这个错误表明这是最好的设计在有限的情况下,所以如果有更好的,我愿意接受想法:D

此外,它不喜欢从构造函数调用的虚拟调用链。是否有一个原因?有办法解决吗? 编辑:具体来说,消息是:“DoNotCallOverridableMethodsInConstructors”。解决方案:“'CompiledLoaderV2.CompiledLoaderV2(String)' 包含一个调用链,该调用链会导致调用该类定义的虚拟方法。请查看以下调用堆栈以了解意外后果” 信息:“不应从构造函数调用在类上定义的虚拟方法。如果派生类重写了该方法,则将调用派生类版本(在调用派生类构造函数之前)”。如果子类在它们的构造函数中做了某些事情,我觉得这可能是个问题,但由于它们没有,我不确定这是一个问题。有没有更好的方法来强制类以某种方式加载而不在构造函数中使用抽象方法?

非常感谢您的帮助!

【问题讨论】:

  • FxCop 到底告诉了你什么?提供更多详细信息以及您的问题是什么,否则这将被视为模糊问题并关闭
  • 明天我会添加 FxCop 消息。我刚离开办公室

标签: c# class nested fxcop


【解决方案1】:

不,您不能对命名空间执行此操作,尽管您可以对程序集执行此操作 - 即阻止任何程序集之外的人创建实例。

您绝对可以使用嵌套类来做到这一点,但您通常应该将构造函数本身设为私有,以防止从该类派生任何其他内容。您还可以将嵌套类本身设为私有,除非您需要将它们提供给外部世界。

您可以使用这种模式来创建类似 Java 枚举的东西,以及有限的工厂。我在 Noda Time 中将它用于歧视性工会 - 实际细节并不重要,但您可能希望查看 the source 以获得更多灵感。

你不相信从构造函数调用虚方法是正确的。它有时会很有用,但应该非常小心地使用大量文档。

【讨论】:

  • 这是一种模式吗?我觉得我有充分的理由这样做,但我不确定是否有更好的模式来这样做。我使用 XmlReader 作为功能参考。
  • @Tigran:仅在嵌套类上,无论如何它都可以是私有的,因此只能可能在相同的源代码中进行扩展。
  • @user652688:是的,当您想要一组明确固定的潜在子类型时,这绝对是一种模式。
【解决方案2】:

考虑将类设为内部。通过这种方式,它们可以在您的程序集中被实例化,但不能由您的库的客户端实例化。出于测试目的,您可以使测试程序集成为您程序集的明确朋友,以便它可以“看到”内部类型并创建它们的实例 - 更适合测试。

【讨论】:

    【解决方案3】:

    这是一个粗略的想法。如果构造函数是公共的(允许您调用它),但需要用户无法获得的东西。

    public interface ILoad
    {
    }
    
    public abstract class LoaderBase : ILoad
    {
        public LoaderBase(InstanceChooser dongle)
        {
          if (dongle == null)
          {
            throw new Exception("Do not create a Loader without an InstanceChooser");
          }
        }
        public abstract void Load(string path);
    }
    
    public class InstanceChooser
    {
        private InstanceChooser()
        {
        }
    
        //construction and initialization
        public static ILoad Create(string path)
        {
            InstanceChooser myChooser = new InstanceChooser();
            LoaderBase myLoader = myChooser.Choose(path);
            if (myLoader != null)
            {
                myLoader.Load(path); //virtual method call moved out of constructor.
            }
            return myLoader;
        }
    
        //construction
        private LoaderBase Choose(string path)
        {
            switch (System.IO.Path.GetExtension(path))
            {
                case "z":  //example constructor call
                    return new CompiledLoaderV1(this);
            }
            return null;
        }
    }
    
    public class CompiledLoaderV1 : LoaderBase
    {
        public CompiledLoaderV1(InstanceChooser dongle)
            : base(dongle)
        {
        }
    
        public override void Load(string path)
        {
            throw new NotImplementedException();
        }
    }
    

    PS,我讨厌返回 null。扔掉的感觉好多了,不必写一百万个空检查。

    【讨论】:

    • 我也更喜欢 throw,但在这种特定情况下,我们假设文件是​​正确的,因此如果出现错误,取消引用 null 或 throwing 似乎大致相同。我不会抓住我的投掷,让应用程序崩溃,或者只是让它在 null deref 上崩溃。不过我喜欢这个主意;我将不得不进一步考虑。谢谢!!
    【解决方案4】:

    已编辑:这是一个示例:

    class Program
    {
        static void Main(string[] args)
        {
            Shape shape = Shape.Create(args[0]);
        }
    }
    
    public abstract class Shape
    {
        protected Shape(string filename) { ...  }
        public abstract float Volume { get; }
        public static Shape Create(string filename)
        {
            string ext = Path.GetExtension(filename);
            // read file here
            switch (ext)
            {
                case ".box":
                    return new BoxShape(filename);
                case ".sphere":
                    return new SphereShape(filename);
            }
            return null;
        }
        class BoxShape : Shape
        {
            public BoxShape(string filename)
                : base(filename)
            {
                // Parse contents 
            }
            public override float Volume { get { return ... } }
        }
        class SphereShape : Shape
        {
            float radius;
            public SphereShape(string filename)
                : base(filename)
            {
                // Parse contents
            }
            public override float Volume { get { return ... } }
        }
    }
    

    它使用嵌套类为具体类创建Shape 的实例,因此用户永远不会为派生类而烦恼。抽象类根据文件扩展名和文件内容选择正确的实现和参数。

    【讨论】:

    • 为什么要嵌套 box 类?意思是,用户是否有理由无法创建自己的盒子?我觉得在我的示例中,目标是防止用户创建错误类型的新对象。在这种情况下,您似乎有一个 Shape Box(float) 和 Shape Sphere(float) 和 Shape Pyramid(float)... 仍然允许用户创建他们想要的。如果你有一个 Shape Create(/* args to create a shape */) 我认为它会更相似。
    猜你喜欢
    • 1970-01-01
    • 2015-06-09
    • 2021-05-11
    • 1970-01-01
    • 2015-04-02
    • 2016-03-02
    • 1970-01-01
    • 2017-10-30
    • 1970-01-01
    相关资源
    最近更新 更多