为什么专用字段专用于类型而不是实例专用?


153

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

public class Foo
{
    private bool aBool;

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

正如C#规范(第3.5.1节,第3.5.2节)所述,对私有字段的访问是基于类型而不是实例。我一直在与一位同事讨论这个问题,我们正试图提出一个使其如此工作的原因(而不是限制对同一实例的访问)。

我们能想到的最好的参数是进行相等性检查,其中类可能要访问私有字段以确定与另一个实例的相等性。还有其他原因吗?还是某些绝对地意味着它必须像这样工作的黄金原因,否则将完全不可能?


18
替代方案有哪些好处?
乔恩·斯基特

19
的确,它不能很好地模拟现实世界。毕竟,仅仅因为我的汽车可以报告剩余的汽油量,并不意味着我的汽车应该能够分辨出每辆汽车剩余的汽油量。因此,是的,从理论上讲,我可以看到具有“私有实例”访问权限。我觉得我实际上永远不会使用它,因为我担心在99%的情况下,我真的想让另一个实例访问该变量。
阿奎那(Aquinas)

2
@Jon所谓的“好处”的立场是,类不应该知道另一个实例的内部状态-不管它是同一类型。每个发言
都没

4
您始终可以使变量成为一对在构造函数中初始化的getter / setter闭包,如果与初始化期间的方法不同,则将失败...
Martin Sojka

8
Scala提供实例私有成员-以及Java,C#和许多其他语言中找不到的许多其他访问级别。这是一个完全合法的问题。
布鲁诺·雷斯

Answers:


73

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

public class Foo
{
    private int bar;

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

    public void Boo()
    {
        Baz(this);
    }
}

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

只需要类型级别而不是对象级别的可见性,就可以确保问题是可解决的,并且可以使情况看起来应该可以实际工作。

编辑:Danilel Hilgarth的论点倒是有道理。语言设计人员可以创建所需的语言,并且编译器编写者必须遵守该语言。话虽这么说,语言设计师确实有一定的动力去使编译器编写者更容易地完成工作。(尽管在这种情况下,很容易争辩说,然后只能通过this(隐式或显式)访问私有成员)。

但是,我认为这使问题比需要解决的更加混乱。如果上面的代码不起作用,大多数用户(包括我自己)会发现它不必要地限制了它:毕竟,这就是要访问的数据!我为什么要经历this

简而言之,我认为我可能夸大了这种情况,因为它对编译器“困难”。我真正想传达的是,上述情况似乎是设计师希望进行的工作。


33
恕我直言,这种推理是错误的方法。编译器强制执行语言规则。它不是使他们。换句话说,如果私有成员是“实例私有”而不是“类型私有”,则编译器将以其他方式实现,例如仅允许this.bar = 2但不允许other.bar = 2,因为other可能是不同的实例。
丹尼尔·希尔加斯

@Daniel很好,但是正如我提到的那样,我认为具有这种限制比具有类级别的可见性更糟。
dlev 2011年

3
@dlev:我没有说“实例私有”成员是一件好事。:-)我只是想指出,您是在说技术的实施决定了语言的规则,而且我认为这种说法是不正确的。
Daniel Hilgarth 2011年

2
@丹尼尔点采取。我仍然认为这是一个有效的考虑因素,尽管您当然正确的是,最终语言设计者会弄清楚他们想要的东西,并且编译器编写者会遵循此要求。
dlev 2011年

2
我认为编译器参数不成立。周围的语言仅允许实例私有(例如Ruby),并允许选择实例私有和类私有(例如Eiffel)。对于这些语言而言,编译器的生成并不需要变得更加困难。有关更多信息,另请参阅我的答案。
亚伯

61

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

例如,如果您在一个类中编写代码,而该类使用另一个类中的某些字段,则这些类之间的联系非常紧密。但是,如果要处理的代码中有两个相同类的对象,则没有额外的依赖关系。一个阶级总是依靠自己。

但是,一旦有人创建属性(或Java中的获取/设置对)并直接公开所有字段,所有关于封装的理论都将失败,这使得类就像他们无论如何都在访问字段一样被耦合。

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


3
我认为一旦get/set提供了方法,它就不会失败。访问器方法仍然保持抽象(用户不需要知道它后面有一个字段,或者该属性后面可以有哪些字段)。例如,如果我们正在编写一个Complex类,则可以公开用于获取/设置极坐标的属性,以及用于获取笛卡尔坐标的另一个get / set。用户不知道该类在下面使用哪个类(它甚至可能是完全不同的东西)。
肯·韦恩·范德林德

5
@Ken:如果为您拥有的每个字段都提供了一个属性,甚至没有考虑是否应该公开它,它的确会失败。
Goran Jovic

@Goran尽管我同意那不是理想的选择,但是它仍然不会完全失败,因为例如,如果基础字段发生更改,则get / set访问器允许您添加其他逻辑。
ForbesLindesay

1
“因为封装的目的是降低不同代码段的相互依赖性” >>否。封装也可以表示实例封装,它取决于语言或思想流派。OO上最权威的声音之一Bertrand Meyer甚至认为这是默认值private。如果愿意,您可以查看我的回答以获取对事物的看法。
亚伯

@Abel:我的回答基于具有类封装的语言,因为OP询问了其中一种语言,并且坦率地说,因为我了解它们。感谢您的更正和新的有趣信息!
Goran Jovic

49

已经向这个有趣的线程添加了很多答案,但是,我还没有找到导致这种行为发生的真正原因。让我试试看:

时光倒流

在80年代的Smalltalk和90年代中期的Java之间的某个地方,面向对象的概念已经成熟。Smalltalk引入了信息隐藏,最初不是将其视为仅面向对象的概念(1978年首次提及),因为类的所有数据(字段)都是私有的,所有方法都是公共的。在90年代OO的许多新发展中,Bertrand Meyer尝试在他的里程碑式的《面向对象的软件构造(OOSC)》一书中将许多OO概念正式化,从那以后,它被视为(几乎)关于OO概念和语言设计的权威性参考。 。

在私密性的情况下

根据迈耶(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#仅允许类封装而不允许实例封装

如果您查看实例封装上的Internet线程(该术语有时用于表示一种事实,即语言在实例级别(而不是类级别)上定义了访问修饰符),那么通常就不会理解该概念。但是,考虑到某些现代语言(至少对于私有访问修饰符)使用实例封装,使您认为它可以并且在现代编程世界中有用。

但是,诚然,C#的语言设计在C ++和Java方面最为艰苦。尽管Eiffel和Modula-3也在其中,但考虑到Eiffel缺少的许多功能(多重继承),我认为在私有访问修饰符方面,他们选择了与Java和C ++相同的路由。

如果您真的想知道为什么您应该尝试获得Eric Lippert,Krzysztof Cwalina,Anders Hejlsberg或从事C#标准工作的任何人。不幸的是,我在带注释的C#编程语言中找不到确定的注释。


1
这是一个非常有趣的答案,它有很多背景,非常有趣,感谢您抽出
宝贵

1
@RichK:不客气,我猜这个问题来得太晚了,但是发现这个话题很有趣,可以进行一定深度的回答:)
Abel

在COM下,“专用”是指“实例-专用”。我认为这在某种程度上是因为类的定义Foo有效地表示了一个接口和一个实现类。由于COM没有类对象引用的概念(仅是接口引用),因此类不可能保存对保证是该类另一个实例的对象的引用。这种基于接口的设计使某些事情变得繁琐,但这意味着一个人可以设计一个可以替换为另一个类的类,而不必共享任何相同的内部结构。
supercat

2
好答案。OP应该考虑将此问题标记为答案。
加文·奥斯本

18

这只是我的意见,但务实的是,我认为,如果程序员可以访问类的源,则可以通过访问类实例的私有成员来合理地信任他们。当您已经在程序员的左边为他们提供了王国的钥匙时,为什么还要捆绑他们呢?


13
我不明白这个说法。假设您正在开发一个程序,在该程序中,是唯一的开发人员,并且是唯一一个永远使用您的库的人,您是在说这种情况下,因为您可以访问源代码,所以使每个成员都公开了吗?
阿奎那

@aquinas:完全不同。将所有内容公开将导致可怕的编程实践。允许类型查看其自身其他实例的私有字段是一个非常狭窄的情况。
Igby Largeman 2011年

2
不,我不是在提倡每个成员公开。天哪!我的论据是,如果您已经在源代码中,可以看到私有成员,如何使用它们,使用约定等,那么为什么不能使用该知识呢?
FishBasketGordo

7
我认为思维方式仍然是类型。如果不能信任该类型以正确处理该类型的成员,该怎么办?(通常,实际上是由程序员来控制交互,但是在这个代码生成工具时代,情况并非总是如此。)
Anthony Pegram

3
我的问题不是真正的信任,更多是必要。当您可以使用反射将对私人的令人发指的事物时,信任是完全不相关的。我想知道为什么从概念上讲您可以访问私人用户。我认为这是合乎逻辑的原因,而不是最佳实践的情况。
RichK 2011年

13

原因的确是相等性检查,比较,克隆,运算符重载……例如,对复杂数字实现operator +将非常棘手。


3
但是您提到的所有内容都需要一种类型来获取另一种类型的值。我同意说:您想检查我的私人国家?好吧继续 您想设定我的私人状态?没办法,哥们,改变你自己的状态。:)
aquinas

2
@aquinas那样行不通。字段是字段。它们仅在声明为as时才是只读的readonly,然后也适用于声明实例。显然,您断言应该有一个特定的编译器规则来禁止这样做,但是每条这样的规则都是C#团队必须规范,记录,本地化,测试,维护和支持的一项功能。如果没有令人信服的好处,那他们为什么要这样做呢?
亚伦诺特,2011年

我同意@Aaronaught的观点:实施每个新规范以及对编译器进行更改都需要付出一定的代价。对于设置的场景,请考虑克隆方法。当然,您可能想使用私有构造函数来实现它,但是在某些情况下,这是不可能/不建议的。
2011年

1
C#团队?我只是在讨论任何语言中可能的理论语言变化。“如果没有令人信服的好处……”我认为可能会有好处。就像任何编译器保证一样,有人可能会发现保证类仅修改其自身状态非常有用。
阿奎那

@aquinas:之所以实现功能,因为它们对许多大多数用户都有用,而不是因为它们在某些孤立的情况下可能有用。
亚伦诺特,2011年

9

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


是的,但是还没有其他解决方法,例如将某些字段公开为内部字段吗?
MikeKulls 2011年

2
@Mike:公开字段internal降低其私密性!您最终会将类的内部组件暴露给程序集中的所有内容。
加布

3

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

虽然这是语言设计师的设计选择。这种访问的限制会使某些非常常见的情况变得极为复杂,而对实体的隔离没有多大贡献。

有一个类似的讨论在这里


1

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

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

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

当然,您始终可以编写私有实例一样编写代码。使用常规get/set方法访问/更改数据。如果该类可能会进行内部更改,那可能会使代码更易于管理。


0

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

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

我从来没有完全接受过这种编程范例,因为它仍然伴随着一整套问题和风险,大多数程序员在此类问题浮出水面并使人们感到困惑并无视拥有私人成员的全部理由之前,并没有完全掌握这些风险。

C#的这种“古怪而又古怪”的方面又是为什么良好的编程与经验和技能无关,而只是知道窍门和陷阱……就像在汽车上工作一样。它的论点是规则应该被打破,这对任何计算语言来说都是非常糟糕的模型。


-1

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

松散地说,我个人认为编写从基类派生的类提供了与您描述的“每个实例具有私有数据”类似的功能。取而代之的是,您仅对每个“唯一”类型都有一个新的类定义。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.