Why are private fields private to the type, not the instance?
在C(和许多其他语言)中,访问同一类型的其他实例的私有字段是完全合法的。例如:
1 2 3 4 5 6 7 8 9 | public class Foo { private bool aBool; public void DoBar(Foo anotherFoo) { if (anotherFoo.aBool) ... } } |
由于C规范(第3.5.1、3.5.2节)规定,对私有字段的访问属于类型,而不是实例。我和一位同事讨论过这个问题,我们正试图找出为什么它会这样工作(而不是限制访问同一个实例)。
我们能想到的最好的参数是进行相等性检查,类可能希望访问私有字段以确定与另一个实例的相等性。还有其他原因吗?或者是一些绝对意味着它必须像这样工作的黄金理由,或者是完全不可能的事情?
我认为这样工作的一个原因是访问修饰符在编译时工作。因此,确定给定对象是否也是当前对象并不容易。例如,考虑以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class Foo { private int bar; public void Baz(Foo other) { other.bar = 2; } public void Boo() { Baz(this); } } |
编译器是否一定能找出
只需要类型级别而不是对象级别的可见性就可以确保问题是可处理的,并且使情况看起来应该实际工作。
编辑:丹尼尔希尔加思的观点,这种推理是向后的,确实有好处。语言设计者可以创建他们想要的语言,编译器编写者必须遵守它。也就是说,语言设计人员确实有一些动机,使编译器编写人员更容易完成他们的工作。(尽管在这种情况下,很容易争辩说私人成员只能通过
然而,我认为这使得这个问题比它需要的更令人困惑。如果上面的代码不起作用,大多数用户(包括我自己)都会发现这是不必要的限制:毕竟,这是我要访问的数据!我为什么要经过
简而言之,我认为我可能夸大了这种情况,因为它对于编译器来说是"困难的"。我真正想了解的是,上面的情况似乎是设计师想要的工作。
因为在C语言和类似语言中使用的封装类型的目的是降低不同的代码块(C和Java中的类)的相互依赖性,而不是内存中的不同对象。
例如,如果您在一个类中编写代码,该类使用另一个类中的某些字段,那么这些类是非常紧密耦合的。但是,如果您处理的代码中有两个同一类的对象,那么就没有额外的依赖关系。类总是依赖于自身。
然而,所有关于封装的理论一旦有人创建属性(或者在Java中获取/设置对)并直接公开所有字段,就会使类耦合起来,就好像它们正在访问字段一样。
有关封装类型的说明,请参阅Abel的最佳答案。
有相当多的答案已经加入到这个有趣的主题中,但是,我并没有找到这个行为之所以如此的真正原因。让我试一试:
回到过去在80的SimultTalk和90年年中的Java之间的某个地方,面向对象的概念日趋成熟。在Smalltalk中,由于类的所有数据(字段)都是私有的,所有方法都是公共的,所以引入了信息隐藏,而不是最初认为只有OO才可用的概念(1978年首次提到)。在90年代OO的许多新发展过程中,Bertrand Meyer试图将其里程碑式的面向对象软件结构(OOSC)中的许多OO概念形式化,从那时起,这被视为(几乎)对OO概念和语言设计的决定性参考。
在私人能见度的情况下根据Meyer的说法,一种方法应该可以用于一组定义的类(第192-193页)。这显然提供了非常高的信息隐藏粒度,ClassA和ClassB及其所有后代都可以使用以下功能:
1 2 | feature {classA, classB} methodName |
在
换句话说:要允许同一类的实例访问,您必须显式地允许该类的方法访问。这有时被称为实例私有与类私有。
编程语言中的实例私有我知道目前使用的至少两种语言使用实例私有信息隐藏,而不是类私有信息隐藏。一种是埃菲尔语,一种由迈耶设计的语言,它把OO推向了极致。另一种是Ruby,一种现在更为常见的语言。在Ruby中,
有人建议编译器很难允许实例私有化。我不这么认为,因为只允许或不允许对方法进行限定调用相对简单。如果对于私有方法,允许使用
从技术角度来看,没有理由选择一种方式或另一种方式(特别是考虑到eiffel.net可以通过IL来实现这一点,即使有多个继承,也没有固有的理由不提供此功能)。
当然,这是一个品味问题,正如其他人已经提到的,如果没有私有方法和字段的类级可见性的特性,相当多的方法可能很难编写。
为什么C只允许类封装而不允许实例封装如果您查看实例封装上的Internet线程(有时一个术语是指一种语言定义实例级的访问修饰符,而不是类级的事实),那么这个概念通常是不受欢迎的。然而,考虑到一些现代语言使用实例封装,至少对于私有访问修饰符来说,这使您认为它可以并且在现代编程世界中使用。
然而,C语言在C语言和Java语言设计方面显然是最难的。虽然埃菲尔和MODEMA-3也在图片中,考虑到Eiffel缺少(多重继承)的许多特征,我相信他们在选择私有访问修饰符时选择了与Java和C++相同的路径。
如果你真的想知道为什么你应该试着联系埃里克·利珀特、科瓦利纳的Krzysztof、安德斯·赫杰斯伯格或任何其他在C标准工作的人。不幸的是,我在注释过的C编程语言中找不到确切的注释。
这只是我的观点,但实际上,我认为如果程序员可以访问类的源代码,您可以合理地信任他们访问类实例的私有成员。当你在程序员的左手边已经给了他们王国的钥匙时,为什么还要用右手绑住他们呢?
原因确实是等式检查、比较、克隆、运算符重载…例如,在复数上实现operator+是非常困难的。
首先,私有静态成员会发生什么?它们只能通过静态方法访问吗?你当然不想这样做,因为那样你就不能访问你的
关于您的明确问题,请考虑一个
1 2 3 4 5 | public class StringBuilder { private string chunk; private StringBuilder nextChunk; } |
如果您不能访问您自己类的其他实例的私有成员,则必须这样实现
1 2 3 4 | public override string ToString() { return chunk + nextChunk.ToString(); } |
这是可行的,但它是O(n^2)——不是很有效。事实上,这很可能会首先破坏拥有一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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),这使得它非常快速,并且只有当您有权访问类的其他实例的私有成员时才可能实现。
这在许多语言中是完全合法的(C++为一)。访问修饰符来自OOP中的封装原理。其思想是限制对外部的访问,在这种情况下,外部是其他类。例如,C中的任何嵌套类也可以访问它的父级私有成员。
而这是语言设计师的设计选择。对这种访问的限制可能会使一些非常常见的场景变得非常复杂,而不会对实体的隔离造成很大影响。
这里也有类似的讨论
我认为我们没有理由不能添加另一个级别的隐私,即数据对每个实例都是私有的。事实上,这甚至可能为语言提供一种完整性的良好感觉。
但在实际操作中,我怀疑它是否真的有用。正如您所指出的,我们通常的私有性对于诸如相等性检查之类的事情以及涉及一个类型的多个实例的大多数其他操作都很有用。不过,我也喜欢您关于维护数据抽象的观点,因为这是OOP中的一个重要观点。
我认为,提供以这种方式限制访问的能力可能是添加到OOP中的一个很好的特性。它真的有用吗?我会说不,因为类应该能够信任自己的代码。因为这个类是唯一可以访问私有成员的东西,所以在处理另一个类的实例时,没有真正的理由需要数据抽象。
当然,您可以编写代码,就好像私有应用于实例一样。使用通常的
上面给出了很好的答案。我要补充的是,这个问题的一部分是这样一个事实,即即使在一开始就允许实例化一个类本身。例如,在一个递归逻辑"for"循环中,只要您有结束递归的逻辑,就可以使用这种类型的技巧。但是,在不创建这样的循环的情况下,在自己内部实例化或传递同一类,在逻辑上会产生自己的危险,即使这是一个被广泛接受的编程范式。例如,C类可以在其默认构造函数中实例化自身的副本,但这不会破坏任何规则或创建因果循环。为什么?
顺便说一句,同样的问题也适用于"受保护"的成员。:(
我从来没有完全接受过这种编程模式,因为它仍然存在一整套问题和风险,大多数程序员在这个问题出现之前并没有完全把握这些问题和风险,使人们感到困惑,并且无视拥有私有成员的全部原因。
C的这一"奇怪和古怪"的方面,也是为什么好的编程与经验和技能无关,而仅仅是了解技巧和陷阱……比如在车上工作的原因之一。它的论点是规则是注定要被打破的,这对于任何计算语言来说都是一个非常糟糕的模型。
在我看来,如果数据对同一类型的其他实例是私有的,那么它就不一定是同一类型了。它似乎不会像其他实例那样表现或行为。可以很容易地根据私有内部数据修改行为。在我看来,这只会让人困惑。
不严格地说,我个人认为编写从基类派生的类提供了与"每个实例都有私有数据"类似的功能。相反,您只需要为每个"unique"类型定义一个新的类。