在Ruby中,受保护的类方法和私有类方法之间有区别吗?

huangapple go评论60阅读模式
英文:

Is there a difference between protected class methods and private class methods in Ruby?

问题

私有类方法和受保护的类方法在Ruby中在可访问性方面的区别是什么?提供了一个用于研究区别的代码示例:

迄今为止,我得出以下结论:

  • 两者都可以从类内部访问,例如self.accessprivatemethodfromwithinsameclassself.accessprotectedmethodfromwithinsameclass
  • 两者都无法从类外部直接访问,例如Aclass.protectedmethodAclass.privatemethod
  • 两者都可以从子类内部访问,例如access...methodwithinsubclass
  • 但是,两者都不能在子类内部直接调用,例如ASubclass.private/protectedmethod

行为上唯一的区别在于当它们无法访问时,受保护的方法会显示以下错误消息:

NoMethodError: protected method ... called for ...Class

而私有方法会显示以下错误消息:

NoMethodError: private method ... called for ...Class

我的问题是:是否存在在行为上有差异的情况?

英文:

I'm trying to understand the difference between private class methods and protected class methods in Ruby in terms of accessibility. I've provided a code example that I am using to study the difference:

class Aclass 
	#public methods below
	def self.accessprivatemethodfromwithinsameclass
		privatemethod 
	end 

	def self.accessprotectedmethodfromwithinsameclass
		protectedmethod
	end 


class << self
	private
		def privatemethod
			"I'm a private method" 
		end 
		
	protected 
		def protectedmethod
			"I'm a protected method" 
		end 
	end
end 

class ASubClass < Aclass
	class << self 
		def accessprivatemethodfromwithinsubclass
			privatemethod
		end 

		def accessprotectedmethodfromwithinsubclass
			protectedmethod
		end 

	end
end 


#Example 
Aclass.protectedmethod 
Aclass.privatemethod
Aclass.accessprivatemethodfromwithinsameclass
Aclass.accessprotectedmethodfromwithinsameclass
ASubClass.accessprivatemethodfromwithinsubclass
ASubClass.accessprotectedmethodfromwithinsubclass
ASubClass.privatemethod
ASubClass.protectedmethod

So far I've concluded that:

  • Both are accessible from within the class ie. as in self.accessprivatemethodfromwitinsameclass and self.accessprotectedfromwithinsameclass
  • Both are not accessible from outside the class as in Aclass.protectedmethod and Aclass.privatemethod.
  • Both are accessible from within the subclass see access...methodwithinsubclass.
  • However both cannot be directly called within the subclass. see ASubclass.private/protectedmethod.

The only difference in behaviour is that when they are not accessible the protected method says:

> NoMethodError: protected method ... called for ...Class

where the private method says:

> NoMethodError: private method ... called for ...Class.

My question is as follows:
is there ever a case where there is a difference in behavior?

答案1

得分: 4

以下是翻译好的部分:

Ruby中没有类方法

首先要理解的一个重要概念是,在Ruby对象模型中不存在类方法这种东西。我们在日常交流中谈论类方法,甚至Ruby核心库中有一些引用类方法的方法(例如Module#public_class_methodModule#private_class_method),但从根本上来说,在对象模型中,它们并不存在。

我们所谓的类方法实际上只是一个普通的单例方法,就像任何对象都可以有的方法一样。只是这个对象恰好是Class类的一个实例。但从根本上讲,Foo的实例和Class的实例之间没有任何区别。

之所以我们为后者使用特殊名称,是因为它们非常常见,而且“一个恰好是Class实例的对象的单例方法”这个说法太长了,所以我们称之为“类方法”。

Ruby中没有单例方法

第二个重要概念是,在Ruby对象模型中也没有单例方法这种东西!我们在日常交流中谈论单例方法,甚至Ruby核心库中有一些引用单例方法的方法(例如Object#singleton_methodObject#singleton_methodsObject#define_singleton_method),但从根本上来说,在对象模型中,它们并不存在。

我们所谓的单例方法实际上只是单例类的普通实例方法。但这只是一个普通的实例方法,没有什么特别之处。单例才是特殊的,而不是方法。但从根本上讲,在任何其他模块中定义的实例方法和在单例类中定义的单例方法之间没有区别。

之所以我们为后者使用特殊名称,是因为区分它们很有用,而且“一个恰好是单例类的模块的实例方法”这个说法太长了,所以我们称之为“单例方法”。

同样,“一个恰好是Class的实例的单例类的模块的实例方法”这个说法也很长,所以我们称之为“类方法”。

Ruby中的消息可访问性

然而,现在我们知道类方法只是单例方法,而单例方法只是实例方法(实际上,在Ruby中只有一种方法,即实例方法,没有单例方法、类方法、静态方法或构造函数),我们可以简单地看一下Ruby中的消息可访问性,因为我们知道类方法没有什么特别之处。

Ruby有三个消息可访问性级别:

  • public(默认级别)
  • private
  • protected

附注:这是在哪里定义的?

不幸的是,与许多其他编程语言不同,Ruby语言规范不存在于一个单一的文档中。Ruby没有一个单一的正式规范来定义某些语言构造的含义。

有几个资源,它们的总和可以被视为Ruby编程语言的规范。

其中一些资源包括:

  • ISO/IEC 30170:2012 信息技术 —— 编程语言 —— Ruby 规范 - 请注意,ISO Ruby规范是在2009年至2010年左右编写的,其特定目标是确保当时所有现有的Ruby实现都容易符合。由于YARV和MacRuby只实现了Ruby 1.9+,MRI只实现了Ruby 1.8及更低版本,而JRuby、XRuby、Ruby.NET和IronRuby(当时)只实现了Ruby 1.8的一个子集,这意味着ISO Ruby规范只包含对Ruby 1.8和Ruby 1.9都通用的功能。此外,ISO Ruby规范旨在保持最小,并仅包含编写Ruby程序所绝对需要的功能。因此,它对String的规范非常广泛(因为它们在Ruby 1.8和Ruby 1.9之间发生了重大变化)。显然,它也没有规定在ISO Ruby规范编写之后添加的功能,比如Ractors或Pattern Matching。
  • Ruby规范套件,即ruby/spec - 但遗憾的是,ruby/spec远未完成。然而,我非常喜欢它,因为它是用Ruby编写的,而不是“ISO标准语言”,对于Ruby程序员来说,这样的编写方式更容易阅读,而且它还可以作为可执行的符合性测试套件。
  • 由David Flanagan和Yukihiro 'matz' Matsumoto编写的Ruby编程语言 - 这本书是由David Flanagan与Ruby的创始人matz共同编写的,旨在作为Ruby的语言参考。
  • 由Dave Thomas、Andy Hunt和Chad Fowler编写的Programming Ruby - 这
英文:

There are no class methods in Ruby

The first important concept to understand is that there is no such thing as a class method in the Ruby Object Model. We talk about class methods colloquially, and there are even methods in the Ruby core library which refer to class methods (e.g. Module#public_class_method and Module#private_class_method), but fundamentally, in the Object Model, they don't exist.

What we call a class method is actually just a normal singleton method like any object can have it. It's just that the object happens to be an instance of the class Class. But fundamentally, there is no difference between a singleton method of an object that is an instance of Foo and an object that is an instance of Class.

The reason we use a special name for the latter is that they are very common, and "singleton method of an object that happens to be an instance of Class" is a mouthful, so we say "class method" instead.

There are no singleton methods in Ruby

The second important concept to understand is that there is no such thing as a singleton method in the Ruby Object Model either! We talk about singleton methods colloquially, and there are even methods in the Ruby core library which refer to singleton methods (e.g. Object#singleton_method, Object#singleton_methods, or Object#define_singleton_method), but fundamentally, in the Object Model, they don't exist.

What we call a singleton method is actually just a normal instance method of the singleton class. But it's a bog standard instance method, there is nothing special about it. It's the singleton class that is special, not the method. But fundamentally, there is no difference between an instance method that is defined in any other module and a singleton method that is defined in a singleton class.

The reason we use a special name for the latter is that it is useful to distinguish them, and "instance method of a module that happens to be a singleton class" is a mouthful, so we say "singleton method" instead.

Likewise, "instance method of a module that happens to be a singleton class of an object that happens to be an instance of Class" is a mouthful, and so way say "class method" instead.

Message accessibility in Ruby

However, since we now know that class method are just singleton methods and singleton methods are just instance methods (and in fact, there is only one kind of method in Ruby, instance methods, no singleton methods, no class methods, no static methods, no constructors), we can simply look at message accessibility in Ruby in general, since we know that there is nothing special about class methods.

Ruby has three message accessibility levels:

  • public (the default)
  • private
  • protected

Aside: where is this defined?

Unfortunately, unlike many other programming languages, the Ruby Language Specification does not exist as a single document in a single place. Ruby does not have a single formal specification that defines what certain language constructs mean.

There are several resources, the sum of which can be considered kind of a specification for the Ruby programming language.

Some of these resources are:

  • The ISO/IEC 30170:2012 Information technology — Programming languages — Ruby specification – Note that the ISO Ruby Specification was written around 2009–2010 with the specific goal that all existing Ruby implementations at the time would easily be compliant. Since YARV and MacRuby only implement Ruby 1.9+ and MRI only implements Ruby 1.8 and lower and JRuby, XRuby, Ruby.NET, and IronRuby (at the time) only implemented a subset of Ruby 1.8, this means that the ISO Ruby Specification only contains features that are common to both Ruby 1.8 and Ruby 1.9. Also, the ISO Ruby Specification was specifically intended to be minimal and only contain the features that are absolutely required for writing Ruby programs. Because of that, it does for example only specify Strings very broadly (since they have changed significantly between Ruby 1.8 and Ruby 1.9). It obviously also does not specify features which were added after the ISO Ruby Specification was written, such as Ractors or Pattern Matching.
  • The Ruby Spec Suite aka ruby/spec – Note that the ruby/spec is unfortunately far from complete. However, I quite like it because it is written in Ruby instead of "ISO-standardese", which is much easier to read for a Rubyist, and it doubles as an executable conformance test suite.
  • The Ruby Programming Language by David Flanagan and Yukihiro 'matz' Matsumoto – This book was written by David Flanagan together with Ruby's creator matz to serve as a Language Reference for Ruby.
  • Programming Ruby by Dave Thomas, Andy Hunt, and Chad Fowler – This book was the first English book about Ruby and served as the standard introduction and description of Ruby for a long time. This book also first documented the Ruby core library and standard library, and the authors donated that documentation back to the community.
  • The Ruby Issue Tracking System, specifically, the Feature sub-tracker – However, please note that unfortunately, the community is really, really bad at distinguishing between Tickets about the Ruby Programming Language and Tickets about the YARV Ruby Implementation: they both get intermingled in the tracker.
  • The Meeting Logs of the Ruby Developer Meetings. (Same problem: Ruby and YARV get intermingled.)
  • New features are often discussed on the mailing lists, in particular the ruby-core (English) and ruby-dev (Japanese) mailing lists. (Same problem again.)
  • The Ruby documentation – Again, be aware that this documentation is generated from the source code of YARV and does not distinguish between features of Ruby and features of YARV.
  • In the past, there were a couple of attempts of formalizing changes to the Ruby Specification, such as the Ruby Change Request (RCR) and Ruby Enhancement Proposal (REP) processes, both of which were unsuccessful.
  • If all else fails, you need to check the source code of the popular Ruby implementations to see what they actually do. Please note the plural: you have to look at multiple, ideally all, implementations to figure out what the consensus is. Only looking at one implementation cannot possibly tell you whether what you are looking at is an implementation quirk of this particular implementation or is a universally agreed-upon behavior of the Ruby Language.

public

public is easy: it has no restrictions. Done.

private

The rule for private accessibility is also very simple: a message send can only invoke a private method if

It is not enough that the receiver is self! For example, this is not allowed:

foo = self
foo.some_private_method

The receiver must be either implicit or it must be the literal pseudo-variable keyword self.

The reason for this restriction is that the implementors of Ruby implementations with highly-sophisticated, aggressively optimizing compilers wanted to be able to statically determine whether a message send was allowed to invoke a private method or not. If you relax this rule to say "a private method can be invoked if the receiver is self", then you can no longer statically determine this, see the following example:

foo = if rand < 0.5 then self else some_other_object end
foo.some_private_method

This is what section 13.3.5.3 Private methods of the ISO/IEC 30170:2012 Information technology — Programming languages — Ruby specification has to say:

> # 13.3.5.3 Private methods
> A private method is a method whose visibility attribute is set to the private visibility.
>
> A private method cannot be invoked with an explicit receiver, i.e., a private method cannot be invoked if a primary-expression or a chained-method-invocation occurs at the position which corresponds to the method receiver in the method invocation, except for a method invocation of any of the following forms where the primary-expression is a self-expression.
>
> * single-method-assignment
> * abbreviated-method-assignment
> * single-indexing-assignment
> * abbreviated-indexing-assignment

Note that the ISO Ruby Programming Language Specification has not been updated since 2012, so this is a slightly outdated definition. The specification was slightly simplified a couple of years ago. Nowadays, the second paragraph should probably read more like this:

> A private method cannot be invoked with an explicit receiver, i.e., a private method cannot be invoked if a primary-expression or a chained-method-invocation occurs at the position which corresponds to the method receiver in the method invocation, except for a method invocation where the primary-expression is a self-expression.

But note that this doesn't make a difference here, since in your code you are not using a self-expression as the receiver anyway, so whether a self-expression is allowed in a limited number of cases or always is irrelevant.

The ruby/spec for private and for message sends also does not seem to be fully up-to-date, but again, the differences do not matter for your question.

The RDoc for Modules, specifically, the subsection on Visibility, however, is fully up-to-date and pretty clear:

> The third visibility is private. A private method may only be called from inside the owner class without a receiver, or with a literal self as a receiver. If a private method is called with a receiver other than a literal self, a NoMethodError will be raised.

protected

In order to be allowed to invoke a protected method, the sender needs to be an instance of the module the invoked method is defined in.

In other words, if you have a protected method m defined in module M like this:

module M
  protected def m; end
end

Then m can only be invoked by a message send where the sender is an instance of M:

kind_of?(M) #=> true
M === self  #=> true

receiver.m

So, in other words, in order for

some_object.some_protected_method

to succeed,

kind_of?(some_object.method(:some_protected_method).owner)

or

some_object.method(:some_protected_method).owner === self

must be true.

[Note: I am assuming that Object#method, Method#owner, Module#===, and Object#kind_of? have not been monkey patched or overridden. The check is not guaranteed to be performed using those actual methods, it is typically performed using internal knowledge by the implementation. In other words, you can't override the behavior of protected by monkey patching or overriding those methods.]

Here's what section 13.3.5.4 Protected methods of the ISO/IEC 30170:2012 Information technology — Programming languages — Ruby specification has to say:

> # 13.3.5.4 Protected methods
> A protected method is a method whose visibility attribute is set to the protected visibility.
>
> A protected method can be invoked if and only if the following condition holds:
>
> * Let M be an instance of the class Module in which the binding of the method exists.
> M is included in the current self, or M is the class of the current self or one of its superclasses.
>
> If M is a singleton class, whether the method can be invoked or not may be determined in an implementation-defined way.

And the RDoc:

> The second visibility is protected. When calling a protected method the sender must inherit the Class or Module which defines the method. Otherwise a NoMethodError will be raised.
>
> Protected visibility is most frequently used to define == and other comparison methods where the author does not wish to expose an object’s state to any caller and would like to restrict it only to inherited classes.

Your examples

Now, let's apply what we learned above to your examples:

> So far I've concluded that:
> - Both are accessible from within the class ie. as in self.accessprivatemethodfromwitinsameclass and self.accessprotectedfromwithinsameclass

The classes are irrelevant here. All that matters is the sender and receiver (for protected) or just the receiver (for private).

privatemethod works, not because it is "within the same class", but because the receiver is implicit.

protectedmethod works, not because it is "within the same class", but because the sender and the receiver are both the same object (the sender is always self, the receiver in this case is implicit, and the implicit receiver is always self), so it is trivially true that the sender is an instance of the receiver's type.

> - Both are not accessible from outside the class as in Aclass.protectedmethod and Aclass.privatemethod.

Again, the classes don't really matter here.

Aclass.privatemethod doesn't work, not because it is "outside the class", but because the receiver is neither implicit (i.e. privatemethod) nor the literal pseudo-variable keyword self (i.e. self.privatemethod).

Aclass.protectedmethod doesn't work, not because it is "outside the class", but because the sender (in this case the un-named top-level object typically called main, which is a direct instance of Object) is not an instance of the module the method is defined in (in this case Aclass.singleton_class).

> - Both are accessible from within the subclass see access...methodwithinsubclass.

Again, the subclass is irrelevant here. In fact, these two cases are exactly identical to the first and second cases from your first bullet point.

privatemethod works, not because it is "within the subclass", but because the receiver is implicit.

protectedmethod works, not because it is "within the subclass", but because the sender and the receiver are both the same object (the sender is always self, the receiver in this case is implicit, and the implicit receiver is always self), so it is trivially true that the sender is an instance of the receiver's type.

> - However both cannot be directly called within the subclass. see ASubclass.private/protectedmethod.

Again, the subclass is not really relevant here. In fact, these two cases are exactly identical to the third and fourth cases from your second bullet point before.

ASubclass.privatemethod doesn't work, not because it is "outside the class", but because the receiver is neither implicit (i.e. privatemethod) nor the literal pseudo-variable keyword self (i.e. self.privatemethod).

ASubclass.protectedmethod doesn't work, not because it is "outside the class", but because the sender (in this case the un-named top-level object typically called main, which is a direct instance of Object) is not an instance of the module the method is defined in (in this case Aclass.singleton_class).

A more interesting example

class Superclass
  class << self
    protected def protected_method; end
  end
end

class Subclass < Superclass
  Superclass.protected_method
end

Is this allowed or not? (Spoiler alert: the answer will first appear to disappoint you, but then satisfy you nonetheless.)

Well, let's check. Remember the rule for protected: the sender must be an instance of the module the invoked method is defined in.

So, the module the invoked method is defined in, is Superclass.singleton_class, i.e. the singleton class of the object Superclass.

The sender is the object Subclass.

So, the question is: is the object Subclass an instance of the class Superclass.singleton_class? The answer to this is non-obvious.

Clearly, Subclass is a subclass of Superclass. But that doesn't help us.

Also, clearly, both Subclass and Superclass are instances of Class (Subclass.class == Class and Superclass.class == Class), which also means they are instances of all of Class's superclasses Module, Object, Kernel, and BasicObject. But that doesn't help us either: the sender must be an instance of the module the invoked method is defined in. It doesn't matter how many other modules the sender and the receiver have in common in their ancestry, they need to specifically have this module in common (in this case Superclass.singleton_class)

Also, by the definition of what the singleton class is, Superclass is an instance of Superclass.singleton_class and Subclass is an instance of Subclass.singleton_class. But that doesn't help us either, since what we would need is for Subclass to be an instance of Superclass.singleton_class.

So, it looks like the answer is: No, this is not allowed.

Except, when you try it out, then it works! But, why?

Maybe you have already spotted the "escape hatch" when I quoted section 13.3.5.4 Protected methods of the ISO/IEC 30170:2012 Information technology — Programming languages — Ruby specification:

> # 13.3.5.4 Protected methods
> A protected method is a method whose visibility attribute is set to the protected visibility.
>
> A protected method can be invoked if and only if the following condition holds:
>
> * Let M be an instance of the class Module in which the binding of the method exists.
> M is included in the current self, or M is the class of the current self or one of its superclasses.
>
> If M is a singleton class, whether the method can be invoked or not may be determined in an implementation-defined way.

In particular, pay attention to the last paragraph [bold italic emphasis mine]:

> If M is a singleton class, whether the method can be invoked or not may be determined in an implementation-defined way.

So, the disappointing answer to the question "does this work" is: We don't know. Ruby doesn't say anything about whether or not it works. It depends on the particular implementation.

From the looks of it, it may or may not work, depending on the implementation.

But fear not!

We have the ruby/spec for singleton classes to our rescue! Here's what it says:

> lang-ruby
> describe "A singleton class" do
> it "is a subclass of a superclass's singleton class" do
> ClassSpecs::K.singleton_class.superclass.should ==
> ClassSpecs::H.singleton_class
> end
> end
>

So, it works because the singleton class of the subclass becomes the superclass of the singleton class of the subclass. Let's unpack that.

When I create a subclass of a superclass:

class Superclass; end
class Subclass < Superclass; end

Then the singleton class of the superclass (Superclass.singleton_class) becomes the superclass of the singleton class (Subclass.singleton_class.superclass).

Or, in other words, for any class C, C.singleton_class.superclass == C.superclass.singleton_class. (Excluding, of course, the top class which has no superclass.)

Since the sender (Subclass) is an indirect instance (Subclass is an instance of its own singleton class Subclass.singleton_class, which in turn is a subclass of Superclass.singleton_class, therefore Subclass is indirectly an instance of Superclass.singleton_class) of the module the invoked method is defined in (Superclass.singleton_class), invoking a protected method is allowed, as per the rules for protected accessibility.

Phew.

And since the ruby/spec applies to all implementations of Ruby (unless explicitly stated otherwise using a guard clause), it is guaranteed that this will be the case in all currently maintained versions of all Ruby implementations.

答案2

得分: 2

这些是封装控制。通常,您会希望坚持使用protected以防止"外部"1代码干扰内部,但更重要的是为了更好地控制您的公共接口,以及契约

任何protected都不应在该类的"家族"之外使用,因此子类可以访问。这是大多数代码使用的方式,因为它通常不关心其他子类。

private方法的方法更具体,将访问权限限制为派生类。

为什么要这样做?这是情境性的,但常见的原因是您正在编写一个库,而您鼓励的模式是对库中的某些类进行子类化。为了保留封装性,其中一些方法是private的。

这意味着您可以在将来更改这些方法而不会为依赖于库的任何代码引入破坏性更改。您将它们标记为private,这意味着"请勿使用,可能会在通知的情况下更改"。

--

1在Ruby中,即使是protectedprivate的代码,您也可以找到一种访问方式,但这被认为是不礼貌和/或有风险的。在其他语言如C++或Java中,编译器通常会拒绝任何此类访问。在Ruby中,您只需行使权力。

英文:

These are encapsulation controls. In general you'll want to stick with protected to keep "outside"<sup>1</sup> code from messing around with internals, but more importantly, to better control your public interface, and by extension, contract.

Anything protected is not intended for use outside of the "family" of that class, so subclasses get access. This is what most code uses as it's generally unconcerned about other subclasses.

The private approach is much more specific and restricts access to derived classes as well.

Why would you want to do this? It's situational, but a common reason is you're writing a library and the pattern you're encouraging is to subclass some classes in the library. In order to preserve encapsulation some of those methods are private.

This means you can those methods in the future without introducing breaking changes for any code dependent on the library. You labeled them private, which means "do not use, subject to change without notice".

--

<sup>1</sup> In Ruby you can always find a way to access code even if it's protected or private, but it is considered rude and/or risky. In other languages like C++ or Java the compiler will generally flat-out refuse any such access. In Ruby you just pull rank.

huangapple
  • 本文由 发表于 2023年5月29日 02:42:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/76353070.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定