为什么私有字段是针对类型而不是实例私有的?

157
在C#(以及许多其他语言)中,访问同一类型的其他实例的私有字段是完全合法的。例如:
public class Foo
{
    private bool aBool;

    public void DoBar(Foo anotherFoo)
    {
        if (anotherFoo.aBool) ...
    }
}

根据C#规范(3.5.1,3.5.2章节)所述,私有字段的访问是基于类型而非实例。我正在与同事讨论这个问题,并试图找出它为什么会这样工作的原因(而不是限制对同一实例的访问)。

我们得出的最佳论据是在相等性检查方面,类可能想要访问私有字段以确定与另一个实例的相等性。还有其他原因吗?或者有一些绝对意味着它必须像这样工作的黄金理由,否则某些事情将完全不可能吗?


18
替代方案有哪些好处? - Jon Skeet
19
确实,它并不能很好地模拟现实世界。毕竟,仅仅因为我的汽车可以报告剩余的油量并不意味着我的汽车应该能够告诉每辆汽车剩余多少油。因此,是的,我理论上可以看到有一个“私有实例”访问权限。但我觉得我真的永远不会用它,因为我会担心在99%的情况下,我真的想要访问另一个实例的变量。 - aquinas
3
@Jon 所谓“优点”的立场是,类不应该知道另一个实例的内部状态 - 无论它是相同类型还是不同类型。这并不是真正的优点,但选择一个而不是另一个可能有很好的理由,这就是我们一直在努力找出的。 - RichK
5
你可以将变量转换为一对getter/setter闭包,在构造函数中初始化,如果_this_在初始化期间与之前不同,则会失败... - Martin Sojka
10
Scala提供了实例私有成员——以及许多其他在Java、C#和许多其他语言中找不到的访问级别。这是一个完全合法的问题。 - Bruno Reis
显示剩余4条评论
10个回答

75

我认为其原因之一是因为访问修饰符在编译时起作用。因此,确定给定对象是否也是当前对象并不容易。例如,请考虑以下代码:

public class Foo
{
    private int bar;

    public void Baz(Foo other)
    {
        other.bar = 2;
    }

    public void Boo()
    {
        Baz(this);
    }
}
编译器能否确信other实际上是this?并非所有情况都是如此。有人可能会说,这样就不应该编译了,但这意味着我们有一条代码路径,其中不能访问正确实例的私有成员,我认为这甚至更糟糕。
仅要求类型级别可见性而非对象级别可见性确保问题可追踪,并使看起来应该工作的情况实际上工作。
编辑:Daniel Hilgarth的观点是合理的,即这种推理是反向的。语言设计者可以创建他们想要的语言,编译器编写者必须遵从它。话虽如此,语言设计者确实有一些激励使编译器编写者更容易完成他们的工作。(尽管在这种情况下,很容易争论私有成员只能通过this(隐式或显式)访问)。
然而,我认为这使问题变得更加混乱。大多数用户(包括我自己)发现如果以上代码不起作用,则会受到不必要的限制!毕竟,那是我试图访问的数据!为什么我必须通过this来访问?
简而言之,我认为我可能夸大了编译器难度的情况。我真正想要传达的是,上述情况似乎是设计人员希望工作的情况。

35
在我看来,这种推理方式是错误的。编译器执行语言规则,而不是创造它们。换句话说,如果私有成员是“实例私有”而不是“类型私有”,那么编译器将以其他方式实现,例如只允许 this.bar = 2 而不允许 other.bar = 2,因为 other 可能是不同的实例。 - Daniel Hilgarth
4
@dlev:我没有提到我是否认为“实例私有”成员是好事情。 :-) 我只是想指出,你在争论一个技术实现决定了语言规则,而在我看来,这种论述是不正确的。 - Daniel Hilgarth
2
@Daniel 理解了。我仍然认为这是一个有效的考虑因素,尽管你当然是正确的,最终语言设计者会决定他们想要什么,编译器编写者会遵循那个方向。 - dlev
1
毕竟,那是我正在尝试访问的数据!为什么我必须经过“这个”呢?你可以将其视为接受任何旧的IEnumerable,传递LinkedList,并尝试在不进行转换的情况下调用.RemoveFirst()。 - Rag
3
我认为编译器参数的论点不成立。有一些语言允许仅实例私有(例如Ruby),并且允许通过选择实例私有和类私有(例如Eiffel)。对于这些语言,生成编译器并不比其他语言更难或更容易。更多信息,请参见我在该线程下的答案。 - Abel
显示剩余5条评论

61

C#和类似语言中使用的封装的目的是降低代码中不同部分(在C#和Java中是类)之间的相互依赖,而不是内存中不同对象之间的依赖关系。

例如,如果你在一个类中编写代码来使用另一个类中的一些字段,那么这些类之间就会非常紧密地耦合。然而,如果你处理的是你有两个同一类的对象的代码,那么就没有额外的依赖关系了。一个类总是依赖于它自己。

然而,所有这些关于封装的理论都失败了,只要有人创建属性(或者在Java中使用get/set对)并直接公开所有字段,这将使得类像访问字段一样紧密耦合。

*关于封装类型的澄清,请参见Abel的优秀回答。


4
在提供了“get/set”方法后,我认为它不会失败。访问器方法仍然维护着一种抽象(用户不需要知道其背后有哪些字段或属性的内容)。例如,如果我们正在编写一个“Complex”类,我们可以公开属性来获取/设置极坐标以及另一个获取/设置笛卡尔坐标的属性。类底层使用的是哪个坐标系对用户来说是未知的(甚至可能是完全不同的东西)。 - Ken Wayne VanderLinde
5
@Ken:如果你为每个字段提供属性而没有考虑是否应该公开它,那么它确实会失败。 - Goran Jovic
尽管我同意那不是最理想的,但是如果底层字段改变,get/set访问器仍然可以允许您添加额外的逻辑。 - ForbesLindesay
2
“因为封装的目的是降低不同代码片段之间的相互依赖性” >> 不,封装也可以意味着实例封装,这取决于编程语言或思想流派。面向对象编程中最具权威性的声音之一Bertrand Meyer甚至认为这是“private”的默认设置。如果您愿意,可以查看我的回答以了解我的观点;)。 - Abel
@Abel:我基于具有类封装的语言回答了这个问题,因为OP问到了其中一种语言,而且说实话,我也比较熟悉这些语言。感谢您的纠正和新的有趣信息! - Goran Jovic
如果允许类型为Foo的实例访问其他实例的私有成员,那么会创建一个依赖关系,任何期望Foo的对象都需要具有这些内部成员的对象,即使它实际上并不关心它们。如果使用类似于Foo.Create()而不是new Foo()这样的东西来创建Foo的实例,则Foo不必具有任何内部成员;创建的东西可以是从Foo派生的ConcreteFoo,但不一定是唯一可以这样做的类。 - supercat

51

这个有趣的主题已经有了很多答案,但我并没有找到真正的原因为什么会出现这种行为。让我试试:

早期的情况

在80年代的Smalltalk和90年代中期的Java之间,面向对象编程的概念得到了成熟。信息隐藏最初并不是面向对象的概念(首次提出于1978年),在Smalltalk中作为一个类的所有数据(字段)都是私有的,所有方法都是公共的。在90年代的众多面向对象开发中,Bertrand Meyer试图在他的里程碑式著作Object Oriented Software Construction (OOSC)中形式化大部分面向对象的概念和语言设计,此书自那时以来一直被认为是(几乎)确定性的面向对象概念和语言设计参考。

对于私有可见性的情况

根据Meyer的说法,应该将方法提供给一组定义好的类(第192-193页)。这显然提供了非常高的信息隐藏粒度,以下功能可用于classA和classB及其所有后代:

feature {classA, classB}
   methodName

private的情况下,他说:如果没有将类型明确声明为对其自身类可见,则无法在限定调用中访问该功能(方法/字段)。也就是说,如果x是一个变量,则不允许x.doSomething()。当然,在类本身内部允许不受限制的访问。
换句话说,要允许同一类的实例访问,必须显式地允许该类访问该方法。这有时被称为实例私有与类私有。
编程语言中的实例私有
我知道至少有两种语言目前正在使用实例私有信息隐藏,而不是类私有信息隐藏。一种是由Meyer设计的Eiffel语言,它将OO推到了极致。另一个是Ruby,现在更常见的语言。在Ruby中,private的意思是:"仅对此实例私有"
语言设计的选择
有人认为允许实例私有对于编译器来说很难。我不这么认为,因为只需要允许或禁止对方法的限定调用就相对简单。对于私有方法,如果允许doSomething(),但不允许x.doSomething(),那么语言设计者就有效地为私有方法和字段定义了仅实例可访问性。
从技术角度来看,选择一种方式或另一种方式没有理由(特别是考虑到Eiffel.NET可以使用IL来实现这一点,即使有多重继承,也没有固有的不提供此功能的原因)。
当然,这是个人口味问题,正如其他人已经提到的,没有类级别可见性的私有方法和字段会更难编写一些方法。
为什么C#只允许类封装而不允许实例封装
如果你查看关于实例封装(有时用于指代语言在实例级别上定义访问修饰符,而不是类级别)的互联网线程,这个概念经常受到批评。然而,考虑到一些现代语言使用实例封装,至少对于私有访问修饰符,这使你认为它可以并且在现代编程世界中是有用的。
然而,C#的确最关注C++和Java的语言设计。虽然Eiffel和Modula-3也在考虑范围内,但考虑到Eiffel缺少的许多特性(多重继承),我认为他们在私有访问修饰符方面选择了与Java和C++相同的路线。
如果你真的想知道为什么,你应该尝试联系Eric Lippert、Krzysztof Cwalina、Anders Hejlsberg或其他任何参与C#标准制定的人。不幸的是,在The C# Programming Language的注释中,我找不到一个明确的说明。

2
这是一个非常好的回答,背景知识很丰富,非常有趣。感谢您抽出时间回复一个(相对)老旧的问题。 - RichK
2
@RichK:不用谢,可能有点晚看到问题了,但是我发现这个话题很有趣,所以用深入的回答来回应 :) - Abel
1
在COM中,“private”表示“实例私有”。我认为这在某种程度上是因为类Foo的定义有效地表示了一个接口和一个实现类;由于COM没有类对象引用的概念——只有接口引用——因此,类无法持有对保证是该类的另一个实例的引用。这种基于接口的设计使得某些事情变得繁琐,但它意味着可以设计一个可替换另一个类的类,而不必共享任何相同的内部内容。 - supercat
3
好的回答。楼主应该考虑将这个回答标记为最佳答案。 - Gavin Osborn

18

仅代表我的观点,但实际上,如果程序员可以访问类的源代码,你可以合理地相信他们可以访问类实例的私有成员。为什么要限制程序员的权利,当你已经将王国的钥匙放在他们的左手中了呢?


14
我不理解这个论点。假设你正在开发一个只有 作为唯一开发者并且只有你会使用你的库的程序,你是不是在这种情况下会将每个成员都设为公共的,因为你可以访问源代码? - aquinas
@aquinas:这是非常不同的。让所有东西都公开将导致可怕的编程实践。允许类型查看其它实例的私有字段是一个非常狭窄的情况。 - Igby Largeman
2
不,我并不主张将每个成员都设为公共的。当然不是!我的论点是,如果你已经在源代码中,可以看到私有成员、它们的使用方式、所采用的惯例等等,那么为什么不能利用这些知识呢? - FishBasketGordo
7
我认为思考方式仍然局限于类型的领域。如果无法信任类型来正确处理其成员,那还能依靠什么呢?(通常情况下,实际上是程序员控制交互,但在这个代码生成工具时代,情况可能并非总是如此。) - Anthony Pegram
3
我的问题并不是关于信任,而是关于必要性。当您可以使用反射访问私有内容时,信任完全无关紧要。我想知道的是,从概念上讲,为什么您能够访问私有内容。我认为这背后应该有一个逻辑上的原因,而不是最佳实践的情况。 - RichK

13

事实上,原因在于等值检查、比较、克隆和运算符重载...例如,在复数上实现operator+会非常棘手。


4
但是你提到的所有内容都需要一种类型获取另一种类型的值。我赞成这样说:你想检查我的私有状态?好的,去检查吧。你想设置我的私有状态?伙计,不行,改变你自己的状态去。 :) - aquinas
2
@aquinas,它不是这样工作的。字段就是字段。只有在声明为“readonly”时才是只读的,然后也适用于声明实例。显然,您断言应该有一个特定的编译器规则来禁止此操作,但每个这样的规则都是C#团队必须规范化、文档化、本地化、测试、维护和支持的功能。如果没有强制性的好处,那么他们为什么要这样做呢? - Aaronaught
我同意@Aaronaught的观点:实现每个新规范和编译器变更都需要成本。对于一个固定的场景,考虑一个克隆方法。当然,你可能想用私有构造函数来实现它,但在某些情况下可能不可行或不建议这样做。 - Lorenzo Dematté
2
C#团队?我只是在讨论任何语言可能的理论语言变化。“如果没有引人注目的好处……”我认为可能有好处。就像使用任何编译器保证一样,有人可能会发现保证类只修改其自身状态很有用。 - aquinas
1
@Aaron "这不是那样运作的。字段就是字段。" 你到底在说什么?允许读取而阻止写入这是微不足道的事情。 - Rag
显示剩余2条评论

9
首先,私有的静态成员会发生什么?它们只能通过静态方法访问吗?这显然是不行的,否则你就无法访问你的常量。
至于你的具体问题,请考虑一个StringBuilder的情况,它被实现为它自己的实例的链表:
public class StringBuilder
{
    private string chunk;
    private StringBuilder nextChunk;
}

如果您无法访问自己类的其他实例的私有成员,您需要像这样实现ToString方法:
public override string ToString()
{
    return chunk + nextChunk.ToString();
}

这种方法可以运行,但是它是O(n^2)的,效率不高。实际上,这可能会打败拥有StringBuilder类的初衷。如果你可以访问自己类的其他实例的私有成员,你可以通过创建正确长度的字符串,然后将每个块的不安全复制到字符串的适当位置来实现ToString
public override string ToString()
{
    string ret = string.FastAllocateString(Length);
    StringBuilder next = this;

    unsafe
    {
        fixed (char *dest = ret)
            while (next != null)
            {
                fixed (char *src = next.chunk)
                    string.wstrcpy(dest, src, next.chunk.Length);
                next = next.nextChunk;
            }
    }
    return ret;
}

这个实现是O(n)的,非常快,只有在您可以访问其他实例私有成员的情况下才能实现。


是的,但难道没有其他方法可以解决这个问题吗?比如将某些字段暴露为内部字段? - MikeKulls
2
@Mike:将字段公开为“internal”会使其不太私有!这样,您最终会将类的内部暴露给其程序集中的所有内容。 - Gabe

4
在许多语言(例如C ++)中,这是完全合法的。访问修饰符来自面向对象编程中的封装原则。其思想是限制对外部的访问,此处的外部指的是其他类。例如,在C#中的任何嵌套类也可以访问其父类的私有成员。
虽然这是语言设计者的设计选择,但限制此访问可能会在不对实体进行隔离的情况下使一些非常常见的场景变得复杂。
这里有一个类似的讨论:here

2

我认为我们可以增加另一层隐私保护,使数据对每个实例都是私有的。事实上,这可能会为语言提供一种完整感。

但在实际应用中,我怀疑它是否真的有那么有用。正如你所指出的,通常的私密性对于诸如平等检查以及涉及多个类型实例的大多数其他操作非常有用。虽然,我也喜欢你提到的维护数据抽象的观点,因为这是面向对象编程中的一个重要点。

总的来说,提供以这种方式限制访问的能力可能是添加到面向对象编程中的一个很好的特性。它真的有那么有用吗?我会说不是,因为一个类应该能够信任自己的代码。由于该类是唯一可以访问私有成员的东西,因此在处理另一个类的实例时没有真正需要数据抽象的原因。

当然,您始终可以将代码编写为如果private应用于实例。使用通常的get/set方法来访问/更改数据。如果该类可能会受到内部更改的影响,则这可能会使代码更易于管理。


0

以上给出了很好的答案。我想补充一点,这个问题的一部分在于类内部实例化自身甚至被允许。例如,在递归逻辑“for”循环中使用这种技巧是有意义的,只要您有逻辑来结束递归。但是,在没有创建这样的循环的情况下实例化或传递相同的类内部会在逻辑上创建自己的危险,尽管这是一个被广泛接受的编程范例。例如,C#类可以在其默认构造函数中实例化自身的副本,但这不会违反任何规则或创建因果循环。为什么?

顺便说一句...这个问题也适用于“protected”成员。:(

我从未完全接受过这种编程范例,因为它仍然带来了一整套问题和风险,大多数程序员直到像这个问题一样的问题出现并困惑人们并违背了拥有私有成员的整个原因时才能完全掌握。

C#的这种“奇怪而古怪”的方面是好的编程与经验和技能无关,而只是知道诀窍和陷阱......就像修车一样。这是规则被打破的论点,这对于任何计算语言来说都是一个非常糟糕的模型。


-1

在我看来,如果数据对于同一类型的其他实例是私有的,那么它就不再是相同的类型了。它似乎不会像其他实例那样表现或行为相同。基于私有内部数据,行为可以很容易地进行修改。在我看来,这只会带来混乱。

笼统地说,我个人认为编写从基类派生的类提供了与“每个实例具有私有数据”所描述的类似功能。相反,您只需针对“唯一”的类型拥有一个新的类定义。


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