为什么我需要使用C#嵌套类?

249

我正在尝试理解C#中的嵌套类。我知道嵌套类是在另一个类内部定义的类,但我不明白为什么会需要这样做。


我想知道还有多少其他问题与这个主题完全相同?拜托,丹!关于C#嵌套类呢? - John Saunders
2
John,我同意这个问题以前被问过,但是在“C#嵌套类”搜索和右边的相关面板中都没有直接重复。怪Stack Overflow的搜索吧 :-{ - H H
7
没错,关于它们有很多资料,但没有说明“为什么”。我找到的最接近的是https://dev59.com/lnRB5IYBdhLWcg3w9b19,你需要知道内部类和嵌套类是同义词。也许这不是搜索问题,而实际上这是在这一层次上的第一个结果。 - John Saunders
5
虽然我真的理解“重复问题”标记的意义,但仍无法抵制内在的情感驱动力,想要分享我的印象:这里的人们对于打“重复”的旗帜非常敏感。虽然他们可能比我更了解编程语言,但似乎在正确解释英语时存在问题,特别是当新手提问时。很高兴看到你们两个关心并理解一个人面临的问题所处的知识水平差异 - 很高兴有你们在这里。 - hardyVeles
8个回答

298

我特别喜欢的一种模式是将嵌套类与工厂模式结合使用:

public abstract class BankAccount
{
  private BankAccount() {} // prevent third-party subclassing.
  private sealed class SavingsAccount : BankAccount { ... }
  private sealed class ChequingAccount : BankAccount { ... }
  public static BankAccount MakeSavingAccount() { ... }
  public static BankAccount MakeChequingAccount() { ... }
}

通过像这样嵌套类,我使第三方无法创建自己的子类。我完全控制任何bankaccount对象中运行的所有代码。而且我的所有子类都可以通过基类共享实现细节。


@EricLippert 如果基类的构造函数是私有的,那么在这种情况下如何调用它?如果无法调用基类构造函数,那么如何创建派生类实例?如果这与帖子无关,请原谅我,我只是想理解代码及其用途。 - Mohit Shah
1
@EricLippert 好的,嵌套成员可以访问包含类型可访问的成员,明白了。 - Mohit Shah
7
没问题。"Private"意味着只能在该类型中访问。嵌套成员根据定义就在该类型中。 - Eric Lippert

131

嵌套类的目的通常只是为了限制其作用域。与普通类相比,嵌套类有额外的可能性,即使用private修饰符(当然还有protected)。

基本上,如果您只需要从“父”类(在作用域方面)中使用此类,则通常将其定义为嵌套类会更合适。如果这个类可能需要被在程序集/库之外使用,那么通常更方便用户将其定义为一个单独的(同级)类,无论这两个类之间是否存在任何概念上的关系。尽管从技术上讲,在public父类中嵌套创建一个public类是可行的,但在我看来,这很少是一个合适的实现方法。


52
实际上,.NET Framework指南明确建议不要创建公共嵌套类。 - Mark Seemann
5
@Henk:我认为Mark指的是这个页面:http://msdn.microsoft.com/en-us/library/tdz1bea9(VS.71).aspx - Noldorin
6
微软没有遵循自己的建议的又一个例子:只需看一下 System.Windows.Forms.ListView 的嵌套类! - Mehrdad Afshari
1
@Henk:实际上,第二行陈述了“公共嵌套类型应该很少使用”。然后它所指的“两个”条件,我不确定(只有一个项目符号)。 - Noldorin
4
这篇MSDN文章需要改进 - http://msdn.microsoft.com/en-us/library/ms229027.aspx。 - akjoshi
显示剩余9条评论

54

嵌套类可以有 privateprotectedprotected internalpublicinternal 访问修饰符。

例如,你正在实现一个GetEnumerator()方法,该方法返回一个IEnumerator<T>对象。使用者并不关心对象的实际类型,他们只知道它实现了该接口。你想要返回的类没有任何直接用途。你可以将该类声明为private嵌套类,并返回其实例(这实际上是C#编译器实现迭代器的方式):

class MyUselessList : IEnumerable<int> {
    // ...
    private List<int> internalList;
    private class UselessListEnumerator : IEnumerator<int> {
        private MyUselessList obj;
        public UselessListEnumerator(MyUselessList o) {
           obj = o;
        }
        private int currentIndex = -1;
        public int Current {
           get { return obj.internalList[currentIndex]; }
        }
        public bool MoveNext() { 
           return ++currentIndex < obj.internalList.Count;
        }
    }
    public IEnumerator<int> GetEnumerator() {
        return new UselessListEnumerator(this);
    }
}

此外,我发现foreach如何内部工作的过程绝对令人惊奇。谢谢! - Karthik
我非常欣赏这个例子。我认为对于许多新的C#开发人员来说,有很多隐藏的假设和未描述的过程。例如,在这里有四个隐藏的接口同时工作:IEnumerable<T>,IEnumerable,IEnumerator<T>和IEnumerator。父类被假定为一个存储数字的List<int>对象。而“foreach”在循环列表时会调用GetEnumerator。错过了太多的复杂性... - Stokely

52

我不明白为什么我需要这样做。

我认为你永远不需要这样做。例如,有一个像这样的嵌套类...

class A
{
  //B is used to help implement A
  class B
  {
    ...etc...
  }
  ...etc...
}

...你总是可以将内部/嵌套类移动到全局范围,就像这样...

class A
{
  ...etc...
}

//B is used to help implement A
class B
{
  ...etc...
}
然而,当B仅用于帮助实现A时,将B作为内部/嵌套类有两个优点:
  • 它不会污染全局作用域(例如,能看到A的客户端代码并不知道B类的存在)。
  • B的方法隐式地访问A的私有成员;如果B不嵌套在A中,则除非那些成员是内部或公共的,否则B将无法访问A的成员;但是,将这些A的方法设为内部或公共的将使这些成员暴露给其他类(而不仅仅是B);因此,将A的方法保持为私有,并通过将B声明为嵌套类来让B访问它们。如果您了解C ++,则类似于在C#中说所有嵌套类自动成为其所包含的类的“朋友”(并且,在C#中声明类为嵌套是声明友谊的唯一方式,因为C#没有“friend”关键字)。

当我说B可以访问A的私有成员时,假设B有对A的引用;它通常会有,因为嵌套类经常像这样声明...

class A
{
  //used to help implement A
  class B
  {
    A m_a;
    internal B(A a) { m_a = a; }
    ...methods of B can access private members of the m_a instance...
  }
  ...etc...
}

...并使用以下类似代码从A的方法构建而来...

//create an instance of B, whose implementation can access members of self
B b = new B(this);

您可以在Mehrdad的回复中看到一个示例。


优点仅限于第一项,如果排除它,则可以使用“protected”,因此“使用私有字段”成为自身的事物。 - jungle_mole
嵌套类不一定是子类(可以使用protected)。例如,外部类可能是某种容器,而嵌套类可能是该容器的迭代器:迭代器希望访问容器的成员数据/实现细节。 - ChrisW

39

公共嵌套成员也有很好的用处...

嵌套类可以访问外部类的私有成员。因此,当创建一个比较器(即实现IComparer接口)时,这种情况是正确的方式。

在这个示例中,FirstNameComparer可以访问私有的_firstName成员,如果该类是一个单独的类,它就不能这样做。

public class Person
{
    private string _firstName;
    private string _lastName;
    private DateTime _birthday;

    //...
    
    public class FirstNameComparer : IComparer<Person>
    {
        public int Compare(Person x, Person y)
        {
            return x._firstName.CompareTo(y._firstName);
        }
    }
}

除此之外,FirstNameComparer 类应该是静态的,以及 Compare 方法。 - ErikE
@erike:如果你像我一样实现了IComparer<T>接口,那就不会出现这种情况了... - Arjan Einbu
我想你是对的! - ErikE
1
你可以使用私有嵌套类并创建静态方法来在顶层类中公开IComparer的实例。因为在这种情况下,嵌套类对其他类没有用处,所以将实例设为私有而不是让其他人控制。 - ni3.net

20

有时候实现一个接口并从类中返回这个接口是很有用的,但这个接口的实现应该完全对外部隐藏。

比如,在C#引入yield之前,实现枚举器的一种方式就是将枚举器的实现作为集合内部的一个私有类。这样可以方便地访问集合的成员,但外部世界不需要/看到它的具体实现细节。


1
这样实现接口的代码有一个额外的好处,即不能将接口偷偷地转换回实现类,因为它是嵌套和私有的,所以无法引用。 - jerryjvl
@jerryjvl 这实际上非常重要!+1,因为它可以使代码更加安全。 - julealgon

6

嵌套类非常有用,可以用于实现不应暴露的内部细节。如果您使用反射器检查像Dictionary<Tkey,TValue>或Hashtable这样的类,您会发现一些示例。


4
也许这是使用嵌套类的好例子?
// ORIGINAL
class ImageCacheSettings { }
class ImageCacheEntry { }
class ImageCache
{
    ImageCacheSettings mSettings;
    List<ImageCacheEntry> mEntries;
}

并且:

// REFACTORED
class ImageCache
{
    Settings mSettings;
    List<Entry> mEntries;

    class Settings {}
    class Entry {}
}

注意:我没有考虑应该应用哪些访问修饰符(private、protected、public、internal)


4
不幸的是,在这种情况下,public Settings Settings { get; set; } 的约定将被忽略。 - Dan Lugg

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