信息隐藏不仅仅是一种约定吗?


20

在Java,C#和许多其他强类型,静态检查的语言中,我们习惯于编写如下代码:

public void m1() { ... }
protected void m2() { ... }
private void m2() { ... }
void m2() { ... }

一些动态检查的语言不提供关键字来表达给定类成员的“私有性”级别,而是依靠编码约定。例如,Python为私有成员添加下划线前缀:

_m(self): pass

可以争论的是,以动态检查的语言提供这样的关键字只会增加用处,因为它只能在运行时检查。

但是,我也找不到以静态检查的语言提供这些关键字的充分理由。我发现需要用相当冗长的关键字(例如protected烦人和分散注意力)填充代码。到目前为止,我还没有遇到过由这些关键字引起的编译器错误会使我免于错误的情况。相反,我一直处在错误放置位置的情况下,protected使我无法使用库。

考虑到这一点,我的问题是:

信息隐藏不仅仅是程序员之间用来定义类正式接口的一部分的约定吗?

可以用来保护班级的秘密状态免遭攻击吗?反射可以覆盖此机制吗?什么使编译器强制实施信息隐藏值得?


10
抱歉,埃里克·利珀特(Eric Lippert)度假:)
杰尔2011年

您的问题有点难以理解。您基本上是在问是否有人可以从外部访问私人会员吗?
摩根Herlocker


@ironcode:我可以说我对某些答案的表达不够清楚。您能说出什么让您难以理解吗?我会尽力澄清。
blubb 2011年

2
“在编译时提供信息,然后仅在运行时检查信息是胡说八道”-真的吗?最好告诉Microsoft,因为这是当前.NET框架的重要组成部分:选择性地选择加入或退出静态类型。我不认为这确实是您的意思;这个问题反复谈到“静态”与“动态”检查,但这是类型系统的特征;这里真正有问题的是编译时运行时绑定
亚伦诺特,2011年

Answers:


4

我正在研究Java认证,其中很多都涉及如何管理访问修饰符。而且它们很有意义,应该正确使用。

我曾经使用过python,在学习过程中,我听说在python中存在约定,因为使用它的人们应该知道它的含义以及如何使用它。就是说,_m(self): pass下划线会提醒我不要搞乱那个领域。但是每个人都会遵循那个约定吗?我使用javascript,我必须说,他们没有。有时我需要检查一个问题,原因是那个人在做他不应该做的事...

阅读有关python领先下划线的讨论

信息隐藏不仅仅是程序员之间用来定义类正式接口的一部分的约定吗?

从我说的来看,是的。

可以用来保护班级的秘密状态免遭攻击吗?反射可以覆盖此机制吗?编译器强制采用的更正式的机制还有其他优点吗?

是的,它可以用于保护类的机密状态,不仅应用于此目的,还应防止用户弄乱对象的状态,以将其行为更改为他们认为应该是对象应具有的方式一直在工作。作为开发人员,您应该进行计划和考虑,并以一种有意义的方式设计您的类,以使其行为不会被篡改。编译器作为一个好朋友,将帮助您按照执行访问策略的方式设计代码。

编辑 是,反射可以,请检查注释

最后一个问题是一个有趣的问题,我愿意阅读有关该问题的答案。


3
反射通常可以绕过访问保护。对于Java和C#,它们都可以访问私有数据。
Mark H

谢谢!答案在这里:stackoverflow.com/questions/1565734/…解释一下!
wleao 2011年

1
Javascript文化与python文化非常不同。Python具有pep-08,几乎所有python开发人员都遵循这些约定。违反PEP-08的行为通常被视为错误。所以我认为javascript / php / perl / ruby​​经验在这方面几乎没有转化为python经验:_private在python中效果很好。
Mike Korobov

1
在某些(但不是全部)编程语言中,信息隐藏可用于防止恶意代码。隐藏信息不能用来防止敌对用户。
布莱恩(Brian)

2
@Mike如果经常将PEP-08违规行为“视为错误”,并且没有Python开发人员会梦想不遵循这些约定,那么您也可以使用该语言来强制执行!语言可以自动完成时,为什么要进行手工工作?
Andres F.

27

“专用”访问说明符与第一次看到它时生成的编译器错误无关。实际上,这是在拥有私有成员的类的实现发生更改时阻止您访问仍会更改的内容。

换句话说,不允许您在它仍在工作时使用它,以防止您在它不再工作时意外地仍在使用它。

正如Delnan在下面提到的前缀约定,只要遵循并正确理解约定,就可以防止意外使用会随时更改的成员。对于恶意(或无知)用户,它不会阻止他们访问该成员,并可能带来所有可能的后果。在具有对访问说明符的内置支持的语言中,这不会无知(编译器错误),并且在恶意时会像酸痛的拇指一样突出(获取私有成员的奇怪构造)。

“受保护的”访问说明符是另一回事-不要认为这仅仅是“不太公开”或“很像私有”。“受保护”表示从包含受保护成员的类派生时,可能使用该功能。受保护的成员是“扩展接口”的一部分,您将使用该接口在现有类之上添加功能,而无需更改这些现有类本身。

因此,简短回顾一下:

  • public:在类的实例上安全使用,该类的目的不会改变。
  • protected:在扩展类(从类派生)时使用-如果实现必须进行大幅度更改,则可能会更改。
  • 私密:请勿触摸!可以随意更改以更好地实现预期的接口。

3
更重要的是,任何未强制执行的“实现细节,随时可能更改”的约定(例如,容易引起注意的下划线)都可以避免意外使用私有成员。例如,在Python中,一个理智的程序员唯一一次使用外部的私有成员编写代码(甚至皱着眉头)的时候,几乎是不可避免的,他确切地知道自己的工作,并接受将来版本中的潜在破坏。 。也就是说,例如Java程序员将使用反射的情况。

3
@Simon Stelling-不,我是说该类的原始实现者告诉您“不要使用它,这里的任何行为将来都可能改变”。例如,出于性能原因,首先包含两个点之间的距离的类的私有成员变量可能随后包含两个点之间的距离平方。任何本来可以依赖旧行为的东西都会悄无声息地打破。
Joris Timmermans

1
@MadKeithV:那么,一个语言级别的关键字说“不要触摸,因为...”与含义相同的约定有何不同?只有编译器强制执行它,但是我们又回到了以前的静态与动态讨论。

7
@Simon Stelling(和Delnan)-对我来说就像是问“发动机限制器与限速标志有何不同”。
Joris Timmermans

1
我很清楚这种差异,但我从未说过不存在。我只是发现您的答案和大多数后续评论仅描述了可以使用约定语言级别的关键字对成员和方法进行注释的类型,而实际的区别(存在语言级别的强制措施)由读者推断。

11

如果您正在编写将被其他人使用的代码,那么信息隐藏可以提供更容易理解的界面。“其他人”可能是您团队中的另一位开发人员,正在使用您以商业方式编写的API的开发人员,或者甚至是将来的自己,他们“只是不记得当初的工作原理”。使用只有4种方法的类比使用40种方法的类要容易得多。


2
我完全同意,但这不能回答我的问题。只要其他程序员理解他应该只查看成员还是不带前缀的成员,我是否在课堂上使用_member还是private member在课堂上都可以忽略不计public
blubb 2011年

6
是的,但是以动态语言编写的类的公共文档也不会将这36个私有部分扔给您(至少默认情况下不是)。

3
@Simon声明它“只是一个惯例”具有误导性,因为它具有真正的好处。“仅是一种约定”就像下一行的花括号一样。具有收益的约定就像使用有意义的名称一样,我会说完全不同。将某项设置为私有会阻止某人看到您的“秘密方法”吗?不,如果已确定,则不会。
Morgan Herlocker 2011年

1
@ Simon-同样,仅公开使用安全的方法可能会增加用户尝试使用该类时不会破坏某些内容的可能性。如果调用一千次,可能很容易使Web服务器崩溃的方法。您不希望API的使用者能够做到这一点。抽象不仅仅是一个约定。
Morgan Herlocker 2011年

4

我建议您从背景开始阅读有关类不变式的背景知识。

长话短说,不变量是关于类状态的假设,该假设在类的整个生命周期中都将保持不变。

让我们使用一个非常简单的C#示例:

public class EmailAlert
{
    private readonly List<string> addresses = new List<string>();

    public void AddRecipient(string address)
    {
        if (!string.IsNullOrEmpty(address))
            addresses.Add(address);
    }

    public void Send(string message)
    {
        foreach (string address in addresses)
            SendTo(address, message);
    }

    // Details of SendTo not shown
}

这里发生了什么?

  • 成员addresses在类构造上初始化。
  • 它是私人的,所以外面没有任何东西可以碰到它。
  • 我们还完成了它readonly,因此构造后内部没有任何东西可以触摸到它(这并不总是正确/必要的,但是在这里很有用)。
  • 因此,该Send方法可以做出一个addresses永远不会成为的假设null。它没有因为要执行检查没办法,该值是可以改变的。

如果允许其他类写入该addresses字段(例如public),则该假设将不再有效。 类中依赖该字段的每个其他方法都必须开始进行显式的null检查,否则可能会使程序崩溃。

所以,是的,这不仅仅是“约定”。类成员上的所有访问修饰符共同构成一组关于何时以及如何更改状态的假设。这些假设随后被并入从属和相互依存的成员和类中,从而使程序员不必同时推理程序的整个状态。进行假设的能力是管理软件复杂性的关键要素。

对于这些其他问题:

可以用来保护班级的秘密状态免遭攻击吗?

是的,没有。与大多数代码一样,安全代码将依赖于某些不变式。访问修饰符作为可靠调用者的路标,当然对他们不应该造成干扰是有帮助的。 恶意代码不会在意,但恶意代码也不必通过编译器。

反射可以覆盖此机制吗?

当然可以。但是反射要求调用代码具有该特权/信任级别。如果您以完全的信任和/或管理特权运行恶意代码,则您已经失去了这场战斗。

什么使编译器强制实施信息隐藏值得?

编译器已经强制执行。.NET,Java和其他此类环境中的运行时也是如此-如果方法是私有的,则用于调用方法的操作码将不会成功。解决该限制的唯一方法是需要可信/提升的代码,提升的代码始终可以直接直接写入程序的内存。在不需要自定义操作系统的情况下,它可以执行得尽可能多。


1
@西蒙:我们需要在这里澄清“无知”。给方法加上前缀_说明合同,但不强制执行。这可能会使人们很难无视合同,但是却并没有使合同破裂变得困难,尤其是与诸如反射之类的乏味且通常具有限制性的方法相比时。一个惯例说,“你不应该打破这一点”。访问修饰符说:“您不能破坏它”。我并不是要说一种方法比另一种方法更好 -两种方法都很好,具体取决于您的哲学,但是它们是非常不同的方法。
亚罗诺

2
打个比方:A private/ protected访问修饰符就像避孕套。您不必使用其中一个,但是如果您不打算使用它,那么最好确定您的时间安排和您的伴侣。它不会阻止恶意行为,甚至可能不会每次都起作用,但是它肯定会降低粗心大意带来的风险,并且比说“请小心”更有效。哦,如果您有一个,但是不使用它,那就不要期望我有任何同情。
2011年

3

信息隐藏不仅仅是一个约定。在很多情况下,尝试解决它实际上可能会破坏类的功能。例如,很常见的做法是将值存储在private变量中,同时使用protectedpublic属性将其公开,然后在getter中检查null并进行必要的初始化(即,延迟加载)。或将某些内容存储在private变量中,使用属性将其公开,然后在setter中,检查该值是否已更改并触发PropertyChanging/ PropertyChanged事件。但是,如果没有看到内部实现,您将永远不知道幕后发生的一切。


3

信息隐藏来自自上而下的设计理念。Python被称为自下而上的语言

在Java,C ++和C#的类级别上,信息隐藏得到了很好的实施,因此在这个级别上,它并不是一个真正的惯例。使用公共接口和隐藏的(私有)详细信息将类设置为“黑匣子”非常容易。

正如您所指出的那样,在Python中,由于一切都可见,因此程序员应遵循不使用要隐藏的内容的约定。

即使使用Java,C ++或C#,在某些时候信息隐藏也成为惯例。在更复杂的软件体系结构中,没有最高抽象级别的访问控制。例如,在Java中,您可以找到“ .internal”的用法。软件包名称。这纯粹是一个命名约定,因为要仅通过程序包可访问性来实施这种信息隐藏并不容易。

努力正式定义访问权限的一种语言是Eiffel。本文指出了Java等语言的其他信息隐藏弱点。

背景:信息隐藏是1971年由David Parnas提出的。他在那篇文章中指出,使用有关其他模块的信息可能“灾难性地增加系统结构的连接性”。根据这个想法,缺乏信息隐藏会导致紧密耦合的系统难以维护。他继续说:

我们希望系统的结构由设计人员在编程开始之前明确确定,而不是由程序员对信息的使用无意中确定。


1
+1表示“程序员对信息的使用”,仅此而已(尽管最后有一个逗号)。
jmoreno,2012年

2

Ruby和PHP拥有并在运行时强制执行。

信息隐藏的点实际上是信息显示。通过“隐藏”内部细节,从外部角度看,目的变得显而易见。有多种语言都包含这一点。在Java中,访问默认为内部软件包,在包中则为protected。您显式声明了它们公开以使其公开。

这样做的全部目的是通过仅公开一个高度一致的接口来使您的类易于使用。您希望其余部分得到保护,这样就不会再有聪明人来干扰您的内部状态,欺骗您的班级去做他想做的事情。

另外,在运行时强制执行访问修饰符时,可以使用它们来强制执行一定级别的安全性,但是我认为这不是一个特别好的解决方案。


1
如果您的图书馆正在与您所使用的公司相同的公司使用,则您将很在意...如果他的应用程序崩溃了,您将很难修复它。
wleao 2011年

1
为什么可以假装系安全带?我改一下这个问题,如果所有信息都可用,编译器是否有理由执行它。您不必等到发生事故才知道该安全带是否值得佩戴。
Mark H

1
@Simon:为什么用您不希望人们使用的东西来弄乱界面。我讨厌C ++要求每个成员都必须存在于头文件的类定义中(我理解为什么)。接口是给其他人的,不应被他们无法使用的东西所困扰。
phkahler 2011年

1
@phkahler:从界面中pydoc删除_prefixedMembers。混乱的接口与编译器检查的关键字的选择无关。
blubb 2011年

2

Acces修饰符绝对可以做约定不做的事情。

例如,在Java中,除非使用反射,否则不能访问私有成员/字段。

因此,如果我为插件编写接口并正确地拒绝通过反射修改私有字段的权限(并设置安全管理器:)),则可以将某些对象发送给任何人实现的功能,并且知道他无法访问其私有字段。

当然,可能会有一些安全漏洞可以使他克服这一问题,但这在哲学上并不重要(但实际上,确实如此)。

如果用户在他的环境中运行我的界面,则他可以控制并因此可以绕开访问修饰符。


2

我还没有遇到过由这些关键字引起的编译器错误会使我免于错误的情况。

避免将应用程序作者从错误中保存下来,只是让库作者决定他们承诺维护其实现的哪一部分。

如果我有图书馆

class C {
  public void foo() { ... }

  private void fooHelper() { /* lots of complex code */ }
}

我可能希望能够替换foo的实现,并可能fooHelper以根本性的方式进行更改。如果fooHelper尽管我的文档中有所有警告,仍然有人决定使用,那么我可能无法做到这一点。

private使库作者可以将库分解为可管理大小的方法(和private帮助程序类),而不必担心会被迫将这些内部细节维护多年。


什么使编译器强制实施信息隐藏值得?

附带说明一下,在Java private中,不是由编译器强制执行,而是由Java 字节码验证程序强制执行。


反射可以覆盖此机制吗?什么使编译器强制实施信息隐藏值得?

在Java中,不仅反射可以覆盖此机制。privateJava 有两种。这种类型private可防止一个外部类访问private由字节码验证程序检查的另一个外部类的成员,但也可以private防止内部类通过包专用的综合访问器方法使用内部对象使用的,

 public class C {
   private int i = 42;

   public class B {
     public void incr() { ++i; }
   }
 }

由于类B(实名C$B)使用i,因此编译器会创建一个综合访问器方法,该方法允许通过字节码验证程序B进行访问C.i。不幸的是,由于ClassLoader允许您从中创建一个类byte[],因此C通过在C包中创建一个新类(如果C未密封jar的话),可以很容易地获得暴露给内部类的私有对象。

正确的private执行要求在类加载器,字节码验证程序和安全策略之间进行协调,以防止反射性访问私有数据。


可以用来保护班级的秘密状态免遭攻击吗?

是。当程序员可以在各自保留其模块安全性的同时进行协作时,“安全分解”是可能的-我不必相信另一个代码模块的作者也不会违反我模块的安全性。

Joe-E这样的对象功能语言使用信息隐藏和其他方法来使安全分解成为可能:

Joe-E是Java编程语言的子集,旨在根据对象能力准则支持安全编程。Joe-E旨在促进安全系统的构建,并促进对Joe-E中构建的系统的安全性审查。

从该页面链接的论文提供了一个示例,说明如何通过private强制实施使安全分解成为可能。

提供安全的封装。

图1.仅追加日志记录工具。

public final class Log {
  private final StringBuilder content;
  public Log() {
    content = new StringBuilder();
  }
  public void write(String s) {
    content.append(s);
  }
}

考虑图1,图1说明了如何构建仅附加日志工具。假设程序的其余部分是用Joe-E编写的,则代码审阅者可以确信只能添加日志条目,而不能对其进行修改或删除。该检查很实用,因为它只需要检查Log 该类,而不需要检查任何其他代码。因此,验证此属性仅需要有关日志记录代码的本地推理。


1

信息隐藏是良好软件设计的主要问题之一。查看70年代后期Dave Parnas的任何论文。基本上,如果不能保证模块的内部状态是一致的,就不能保证其行为。保证其内部状态的唯一方法是保持其私有状态,并仅允许通过提供的方式对其进行更改。


1

通过使保护成为语言的一部分,您将获得一些好处:合理的保证。

如果我将变量设为私有,则我有合理的保证,只有该类中的代码或该类的显式声明的朋友才能触摸它。可以合理地触及该值的代码范围受到限制并明确定义。

现在有办法避开语法保护吗?绝对; 大多数语言都有它们。在C ++中,您始终可以将类强制转换为其他类型,然后按其位戳。在Java和C#中,您可以将自己反映在其中。依此类推。

但是,这样做很难。很明显,您正在做不应该做的事情。您不能无意间做到这一点(C ++中的狂野写意之外)。您必须乐意考虑:“我将碰到编译器告诉我不要执行的操作。” 您必须乐意做一些不合理的事情。

没有语法保护,程序员可能会不小心搞砸了。您必须教给用户一个约定,并且他们每次都必须遵循该约定。如果他们不这样做,世界将变得非常不安全。

没有句法保护,责任就在于错误的人:使用阶级的许多人。它们必须遵循约定,否则会发生未指定的不良情况。从头开始:可能会发生未指定的不良情况。

没有什么比API更糟糕的了,如果您做错了事,一切都会正常运行。这给用户提供了错误的保证,即他们做对了事情,一切都很好,等等。

那该语言的新用户又是什么呢?他们不仅必须学习并遵循实际的语法(由编译器强制执行),而且现在还必须遵循此约定(最多由其同级强制执行)。如果他们不这样做,那么就不会有坏事发生。如果程序员不理解约定为什么存在,会发生什么?当他说“拧”并随便戳您的私人物品时会发生什么?如果一切正常,他会怎么看?

他认为惯例很愚蠢。而且他再也不会遵循它。他会告诉他的所有朋友也不要打扰。

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.