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

在 C # (和许多其他语言)中,访问同一类型的其他实例的私有字段是完全合法的。例如:

public class Foo
{
private bool aBool;


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

正如 C # 规范(3.5.1,3.5.2节)所述,对私有字段的访问是在一个类型上,而不是在一个实例上。我一直在和一个同事讨论这个问题,我们正试图找出为什么它是这样工作的原因(而不是限制对同一个实例的访问)。

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

8968 次浏览

这只是我个人的观点,但是从实用的角度来说,我认为如果程序员能够访问类的源代码,那么您就可以合理地信任他们来访问类实例的私有成员。既然你已经给了程序员通往王国的钥匙,为什么还要束缚他们的右手呢?

原因确实是平等检查、比较、克隆、运算符重载... ..。 例如,在复数上实现运算符 + 将是非常棘手的。

在我看来,如果数据对于同一类型的其他实例是私有的,那么它就不一定再是同一类型了。它的行为和其他实例似乎不一样。可以根据该私有内部数据轻松修改行为。那只会在我看来造成混乱。

不严格地说,我个人认为从基类派生的类提供了类似的功能,就像你描述的“每个实例拥有私有数据”。相反,您只需为每个“惟一”类型创建一个新的类定义。

因为 C # 和类似语言中使用的封装类型 * 的目的是降低不同代码片段(C # 和 Java 中的类)之间的相互依赖性,而不是降低内存中不同对象之间的相互依赖性。

例如,如果在一个类中编写的代码使用另一个类中的某些字段,那么这些类是非常紧密耦合的。但是,如果处理的代码中有两个同一类的对象,则不存在额外的依赖项。类总是依赖于它自己。

然而,只要有人创建属性(或 Java 中的 get/set 对)并直接公开所有字段,所有这些关于封装的理论就会失败,这使得类成为耦合的,就好像它们无论如何都要访问字段一样。

* 关于封装的种类,请参阅 Abel 的精彩回答。

这在许多语言中是完全合法的(C + + 就是其中之一)。访问修饰符来自于 OOP 中的封装原则。这个想法是为了限制对 在外面的访问,在这种情况下,外部是其他类。例如,C # 中的任何嵌套类都可以访问它的父类私有成员。

虽然这是语言设计师的设计选择。这种访问的限制可能会使一些非常常见的场景变得非常复杂,而不会对实体的隔离造成很大影响。

有一个类似的讨论 给你

我不认为我们不能增加另一个隐私级别的原因,那就是数据对每个实例都是私有的。事实上,这甚至可以为语言提供一种完整的感觉。

但在实际操作中,我怀疑它是否真的有用。正如您指出的,我们通常的私有性对于诸如相等性检查以及涉及多个 Type 实例的大多数其他操作都很有用。不过,我也喜欢你关于维护抽象化的观点,因为这是面向对象编程中的一个重要观点。

我认为,以这种方式提供限制访问的能力可能是添加到 OOP 中的一个很好的特性。这真的有用吗?我会说不,因为类应该能够信任自己的代码。因为这个类是唯一可以访问私有成员的东西,所以在处理另一个类的实例时没有必要使用抽象化。

当然,您总是可以编写应用于实例的 好像私有代码。使用通常的 get/set方法访问/更改数据。如果类可能会受到内部更改的影响,那么这可能会使代码更易于管理。

我认为它这样工作的一个原因是因为访问修饰符在 编译时上工作。因此,确定给定对象是否也是 目前对象并不容易。例如,考虑下面的代码:

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

简而言之,我认为我可能夸大了情况是“困难”的编译器。我真正想要表达的是,上面的情况看起来像是设计师希望有工作的情况。

首先,私有静态成员会发生什么情况?只能通过静态方法访问它们吗?你肯定不希望这样,因为那样你就不能访问你的 consts。

至于你明确的问题,考虑一下 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) ,这使得它非常快,它是 只有在您可以访问类的其他实例的私有成员的情况下才有可能

相当多的答案已经添加到这个有趣的线程,然而,我没有完全找到 为什么这种行为的真正原因是它的方式。让我试试看:

很久以前了

在80年代的 Smalltalk 和90年代中期的 Java 之间,面向对象的概念成熟了。信息隐藏最初并没有被认为是只有面向对象(OO)才能使用的概念(1978年首次提到) ,它是在 Smalltalk 中引入的,因为一个类的所有数据(字段)都是私有的,所有方法都是公有的。在90年代 OO 的许多新发展中,Bertrand Meyer 试图在他的里程碑式的书 面向对象软件构造(OOSC)中形式化大量的 OO 概念,这本书从那时起被认为是(几乎)关于 OO 概念和语言设计的权威参考。

在私有可见性的情况下

根据 Meyer 的说法,一个方法应该可用于一组已定义的类(第192-193页)。这显然提供了一个非常高的信息隐藏粒度,类 A 和类 B 及其所有后代都可以使用以下特性:

feature {classA, classB}
methodName

private的例子中,他说了以下几点: 如果没有显式地声明类型对它自己的类是可见的,那么就不能在限定调用中访问该特性(方法/字段)。也就是说,如果 x是一个变量,x.doSomething()是不允许的。当然,在类本身内部允许非限定访问。

换句话说: 要允许同一个类的实例访问,必须显式地允许该类的方法访问。这有时被称为实例-私有和类-私有。

实例-编程语言中的私有实例

我知道至少有两种当前使用的语言使用实例-私有信息隐藏,而不是类-私有信息隐藏。其中之一是由 Meyer 设计的 Eiffel 语言,它将 OO 发挥到了极致。另一个是 Ruby,它现在是一种通用得多的语言。在 Ruby 中,private的意思是: “此次事件的隐私”

语言设计的选择

有人建议,允许实例私有对编译器来说是困难的。我不这么认为,因为只允许或不允许对方法的限定调用相对简单。如果对于私有方法,允许使用 doSomething()而不允许使用 x.doSomething(),那么语言设计人员已经有效地为私有方法和字段定义了仅实例可访问性。

从技术的角度来看,没有理由选择一种或另一种(尤指。当考虑到埃菲尔. NET 可以使用 IL 做到这一点,即使使用多重继承,也没有内在的理由不提供这一特性)。

当然,这是一个品味问题,正如其他人已经提到的,如果没有私有方法和字段的类级可见性特性,相当多的方法可能更难编写。

为什么 C # 只允许类封装而不允许实例封装

如果您查看实例封装上的互联网线程(这个术语有时用于指一种语言在实例级别上定义访问修饰符,而不是类级别) ,这个概念常常会遭到反对。但是,考虑到一些现代语言使用实例封装(至少对于私有访问修饰符是这样) ,使您认为它可以并且正在现代编程世界中使用。

然而,不可否认的是,C # 在 C + + 和 Java 的语言设计方面最为努力。虽然 Eiffel 和 Modula-3也在其中,但考虑到 Eiffel 缺少的许多特性(多重继承) ,我相信它们在私有访问修饰符方面选择了与 Java 和 C + + 相同的路径。

如果你真的想知道 abc 0,你应该试着联系一下埃里克 · 利伯特、克兹斯托夫 · 卡瓦利纳、安德斯·海尔斯伯格或者其他任何一个研究 c # 标准的人。不幸的是,我在带注释的 C # 编程语言中找不到确切的注释。

上面给出的答案很棒。我要补充的是,这个问题的部分原因是,实例化一个类本身甚至是允许放在首位的。例如,在递归逻辑“ for”循环中,只要有结束递归的逻辑,就可以使用这种类型的技巧。但是,在自身内部实例化或传递同一个类,而不创建这样的循环,在逻辑上会造成自身的危险,尽管这是一个被广泛接受的编程范型。例如,一个 C # 类可以在它的缺省构造函数中实例化它自己的一个副本,但是这不会破坏任何规则或者创建因果循环。为什么?

顺便说一下... ... 同样的问题也适用于“受保护的”成员

我从来没有完全接受这个编程范型,因为它仍然伴随着一系列的问题和风险,大多数程序员没有完全理解这些问题,直到像这样的问题出现,使人们感到困惑,挑战拥有私人成员的全部理由。

C # 的这种“怪异和古怪”的方面是为什么好的编程与经验和技能无关,而只是知道技巧和陷阱... ... 就像在汽车上工作一样的又一个原因。它认为规则就是要被打破的,这对于任何计算语言来说都是一个非常糟糕的模型。