为啥私有字段是类型私有的,而不是实例私有的?

Posted

技术标签:

【中文标题】为啥私有字段是类型私有的,而不是实例私有的?【英文标题】:Why are private fields private to the type, not the instance?为什么私有字段是类型私有的,而不是实例私有的? 【发布时间】:2011-10-22 10:48:48 【问题描述】:

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

public class Foo

    private bool aBool;

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

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

我们能想到的最好的论点是用于相等性检查,其中类可能希望访问私有字段以确定与另一个实例的相等性。还有其他原因吗?或者某些黄金理由绝对意味着它必须像这样工作,否则完全不可能?

【问题讨论】:

替代方案有什么好处? 确实它不能很好地模拟现实世界。毕竟,仅仅因为我的车可以报告它还剩多少油,并不意味着我的车应该能够告诉每辆车还剩多少油。所以,是的,理论上我可以看到拥有“私有实例”访问权限。我觉得我真的永远不会使用它,因为我担心在 99% 的情况下,我真的希望让另一个实例访问该变量。 @Jon 对于所谓的“好处”所采取的立场是,类不应该知道另一个实例的内部状态——不管它是同一类型的事实。不是每个人说的好处,但可能有一个很好的理由选择一个而不是另一个,这就是我们一直试图弄清楚的 您总是可以将变量放入一对 getter/setter 闭包中,在构造函数中初始化,如果 this 与在初始化... Scala 提供了 instance private 成员 -- 以及 Java、C# 和许多其他语言中没有的许多其他访问级别。这是一个完全合法的问题。 【参考方案1】:

上面给出了很好的答案。我要补充的是,这个问题的一部分是,在其内部实例化一个类甚至是首先被允许的。例如,只要您有结束递归的逻辑,它就会在递归逻辑“for”循环中使用这种类型的技巧。但是,在不创建此类循环的情况下在其内部实例化或传递相同的类在逻辑上会产生其自身的危险,即使它是一种被广泛接受的编程范式。例如,C# 类可以在其默认构造函数中实例化自身的副本,但这不会破坏任何规则或创建因果循环。为什么?

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

我从来没有完全接受这种编程范式,因为它仍然伴随着一整套问题和风险,大多数程序员都没有完全理解这些问题和风险,直到像这样的问题出现并让人们感到困惑并无视拥有私人成员的全部理由。

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

【讨论】:

【参考方案2】:

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

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

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

*有关封装类型的说明,请参阅 Abel 的出色回答。

【讨论】:

我认为一旦提供了get/set 方法,它就不会失败。访问器方法仍然保持抽象(用户不需要知道它后面有一个字段,或者属性后面可能有什么字段)。例如,如果我们正在编写一个Complex 类,我们可以公开属性来获取/设置极坐标,以及另一个获取/设置笛卡尔坐标。用户不知道该类在下面使用哪一个(甚至可能完全不同)。 @Ken:如果你为你拥有的每个字段都提供了一个属性,甚至没有考虑是否应该公开它,它确实会失败。 @Goran 虽然我同意这并不理想,但它仍然不会完全失败,因为 get/set 访问器允许您在底层字段发生变化时添加额外的逻辑。 “因为封装的目的是为了降低不同代码段的相互依赖” >> no.封装也可以意味着实例封装,它取决于语言或思想流派。 OO 中最权威的声音之一 Bertrand Meyer 认为这甚至是 private 的默认值。如果您愿意,可以查看我的回答以了解我对事物的看法;)。 @Abel:我的回答基于具有类封装的语言,因为 OP 询问了其中一种,坦率地说是因为我了解它们。感谢您的更正和新的有趣信息!【参考方案3】:

相当多的答案已经添加到这个有趣的线程中,但是,我并没有完全找到为什么这种行为的真正原因。让我试一试:

回到过去

在 80 年代的 Smalltalk 和 90 年代中期的 Java 之间的某个地方,面向对象的概念已经成熟。信息隐藏,最初不被认为是仅适用于 OO 的概念(在 1978 年首次提到),在 Smalltalk 中被引入,因为类的所有数据(字段)都是私有的,所有方法都是公共的。在 90 年代 OO 的许多新发展中,Bertrand Meyer 试图在他的标志性著作Object Oriented Software Construction (OOSC) 中将大部分 OO 概念形式化,这本书从那时起被认为是(几乎)关于 OO 概念和语言设计的权威参考。

在私有可见性的情况下

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

feature classA, classB
   methodName

private 的情况下,他说以下内容:如果不明确声明一个类型对其自己的类可见,则无法在合格的调用中访问该功能(方法/字段)。 IE。如果x 是一个变量,则不允许x.doSomething()。当然,在类本身内部允许不合格的访问。

换句话说:要允许同一类的实例访问,您必须明确允许该类的方法访问。这有时被称为实例私有与类私有。

编程语言中的实例私有

我知道目前使用的至少两种语言使用实例私有信息隐藏而不是类私有信息隐藏。一种是 Eiffel,一种由 Meyer 设计的语言,它将 OO 发挥到了极致。另一种是 Ruby,一种如今更为常见的语言。在 Ruby 中,private 表示:"private to this instance"。

语言设计的选择

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

从技术的角度来看,没有理由选择一种方式或另一种方式(尤其是考虑到 Eiffel.NET 可以使用 IL 做到这一点,即使是多重继承,也没有内在的理由不提供此功能) .

当然,这是个人喜好问题,正如其他人已经提到的那样,如果没有私有方法和字段的类级别可见性特性,有些方法可能更难编写。

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

如果您查看有关实例封装的 Internet 线程(该术语有时用于指代一种语言在实例级别而不是类级别定义访问修饰符的事实),该概念通常不受欢迎。然而,考虑到一些现代语言使用实例封装,至少对于 private 访问修饰符,让您认为它可以并且在现代编程世界中有用。

但是,C# 的语言设计无疑是最看重 C++ 和 Java 的。虽然 Eiffel 和 Modula-3 也在图中,但考虑到 Eiffel 缺少的许多特性(多重继承),我相信他们在私有访问修饰符方面选择了与 Java 和 C++ 相同的路线。

如果您真的想知道为什么,您应该尝试联系 Eric Lippert、Krzysztof Cwalina、Anders Hejlsberg 或其他任何从事 C# 标准工作的人。不幸的是,我在带注释的The C# Programming Language 中找不到明确的注释。

【讨论】:

这是一个有很多背景的可爱答案,非常有趣,感谢您抽出时间回答一个(相对)老问题 @RichK:不客气,我猜只是发现问题太晚了,但发现这个话题足够有趣,可以深入回答:) 在 COM 下,“private”的意思是“instance-private”。我认为这在某种程度上是因为类Foo 的定义有效地表示了一个接口和一个实现类;因为 COM 没有类-对象引用的概念——只是一个接口引用——一个类不可能持有对保证是该类的另一个实例的东西的引用。这种基于接口的设计使一些事情变得繁琐,但这意味着人们可以设计一个可以替代另一个类的类,而不必共享任何相同的内部结构。 很好的答案。 OP 应该考虑将此问题标记为答案。【参考方案4】:

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

public class Foo

    private int bar;

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

    public void Boo()
    
        Baz(this);
    

编译器能否确定other 实际上是this?并非在所有情况下。有人可能会争辩说,这只是不应该编译,但这意味着我们有一个代码路径,其中无法访问正确实例的私有实例成员,我认为这更糟。

只需要类型级别而不是对象级别的可见性可确保问题易于处理,并使情况看起来应该工作实际上工作。

编辑:Daniel Hilgarth 认为这种推理是倒退的观点确实有道理。语言设计者可以创建他们想要的语言,编译器编写者必须遵守它。话虽如此,语言设计者确实有一些动机让编译器编写者更容易完成他们的工作。 (尽管在这种情况下,很容易争辩说私有成员只能通过this(隐式或显式)访问)。

但是,我认为这会使问题变得比需要的更加混乱。如果上面的代码不起作用,大多数用户(包括我自己)会发现它不必要地限制:毕竟,这是我试图访问的 my 数据!为什么一定要通过this

简而言之,我认为我可能夸大了编译器“困难”的情况。我真正想要表达的是,上述情况似乎是设计师想要的工作。

【讨论】:

恕我直言,这种推理是错误的。编译器强制执行语言规则。它不会制造它们。换句话说,如果私有成员是“实例私有”而不是“类型私有”,则编译器将以其他方式实现,例如只允许this.bar = 2 但不允许other.bar = 2,因为other 可能是不同的实例。 @Daniel 说得很好,但正如我所提到的,我认为有这个限制比拥有类级别的可见性更糟糕。 @dlev:我没有说我是否认为“实例私有”成员是一件好事。 :-) 我只是想指出,您在争辩说技术实现决定了语言的规则,而在我看来,这种论点是不正确的。 @Daniel Point 已采取。我仍然认为这是一个有效的考虑因素,尽管您当然是正确的,最终语言设计者会弄清楚他们想要什么,而编译器编写者会遵守这一点。 我认为编译器参数不成立。周围有一些语言只允许实例私有(如 Ruby),并且允许选择实例私有和类私有(如 Eiffel)。对于这些语言来说,编译器的生成并不需要更难或更容易。有关更多信息,另请参阅我的回答。【参考方案5】:

首先,私有静态成员会发生什么?它们只能通过静态方法访问吗?您当然不希望这样,因为那样您将无法访问您的 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),这使得它非常快,并且只有当你可以访问你的类的其他实例的私有成员时才有可能

【讨论】:

是的,但是没有其他方法可以解决这个问题,例如将某些字段公开为内部字段吗? @Mike:将字段公开为internal 使其更少私有!你最终会将你的类的内部暴露给它的程序集中的所有东西。【参考方案6】:

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

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

有类似的讨论here

【讨论】:

【参考方案7】:

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

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

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

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

【讨论】:

【参考方案8】:

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

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

【讨论】:

【参考方案9】:

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

【讨论】:

但是您提到的所有内容都需要一种类型获取另一种类型的值。我同意说:你想检查我的私人状态吗?好吧,继续。你想设置我的私人状态吗?没办法,伙计,改变你自己该死的状态。 :) @aquinas 那样不行。字段是字段。它们仅在声明为 readonly 时是只读的,然后这也适用于声明实例。显然,您是在断言应该有一个特定的编译器规则来禁止这种情况,但每个这样的规则都是 C# 团队必须规范、记录、本地化、测试、维护和支持的特性。如果没有令人信服的好处,那么他们为什么要这样做? 我同意@Aaronaught:实施每个新规范和对编译器进行更改都会产生成本。对于一组场景,请考虑使用克隆方法。当然,您可能希望使用私有构造函数来实现它,但在某些情况下,它可能是不可能的/不可取的。 C# 团队?我只是在讨论任何语言中可能的理论语言变化。 “如果没有令人信服的好处……”我认为可能会有好处。就像 any 编译器保证一样,有人可能会发现保证类只修改自己的状态很有用。 @aquinas:功能得以实现是因为它们许多大多数用户有用,而不是因为它们可能在一些孤立的情况下有用。【参考方案10】:

这只是我的观点,但从实际角度来说,我认为如果程序员可以访问类的源代码,您可以合理地信任他们访问类实例的私有成员。你已经给了他们打开王国的钥匙,为什么还要绑定程序员的右手呢?

【讨论】:

我不明白这个论点。假设您正在开发一个程序,其中 是唯一的开发人员,并且您是唯一会使用您的库的人,您是说在这种情况下您会公开每个成员,因为您可以访问源代码? @aquinas:这完全不同。将所有内容公开将导致可怕的编程实践。允许一个类型查看其自身其他实例的私有字段是一种非常狭隘的情况。 不,我不主张让每个成员都公开。天哪!我的论点是,如果您已经在源中,可以看到私有成员、它们的使用方式、使用的约定等,那么为什么不能使用这些知识呢? 我觉得思路还是在类型方面。如果不能信任该类型来正确处理该类型的成员,那还能做什么? (通常,实际控制交互的是程序员,但在这个代码生成工具时代,情况可能并非总是如此。) 我的问题并不是真正的信任,更多的是必要性。当您可以对令人发指的事情进行反思时,信任就完全无关紧要了。我想知道为什么从概念上讲,您可以访问私人信息。我认为这是合乎逻辑的原因,而不是最佳实践情况。

以上是关于为啥私有字段是类型私有的,而不是实例私有的?的主要内容,如果未能解决你的问题,请参考以下文章

为啥在接口列表的泛型类型中使用私有嵌套类型不是“不一致的可访问性”?

为啥这里的 String 构造函数应该被保护而不是私有?

C#中继承类为啥可以通过属性访问基类的私有字段。

为啥同一类而不是同一对象可以访问受保护和私有属性?

为啥 Rust 认为我的私有类型必须是公共的,除非我使用 pub(crate)?

为啥我可以在私有类型上使用 auto ?