[nikic master からの翻訳] PHP のネイティブ型のメソッド
CET-4 の英語レベルを使用して翻訳するのはこれが初めてです~~囧,不正確な点も多々あるかと思いますが、適当すぎる場合は、お気軽にコメントください(適当な訳が思い浮かばない箇所もあるので、原文と私の拙い訳を載せておきます)。
この記事を翻訳するのに 3 晩と 1 昼かかりました~。まず、それについて話す前に一般的な技術的な側面を理解し、それから翻訳する必要があります~
これは英語を訓練する一方で、比較的新しい技術的アイデアを海外から学ぶためです。
この記事では主に、PHP でのネイティブ型に対するオブジェクト指向操作の実装について説明します。これは、PHP における不規則な関数名、不規則なパラメーターの順序、および可読性の低さの問題を解決するために拡張機能を使用して実装されます。
この拡張機能は、ZEND エンジンでオブジェクト指向メソッドを呼び出すときに、対応する処理関数を変更することで実装されます。IS_STRING の場合はカスタム処理を続行し、オブジェクト指向の呼び出し元のタイプを決定する関数を登録します。 ZEND のデフォルト処理関数に戻り、以下の説明を参照してください。
HOOKPHP コード
PHP はインタープリタ言語であり、コードは次の関数によって中間バイトコードに変換されます。 ZEND エンジンが解析して実行します。 PHP は中間バイトコード OPCODE を呼び出します。各 OPCODE は ZEND の下部の処理関数に対応し、最終的にこの処理関数が実行されます。 HOOK 関数を実装するには、HOOK OPCODE に対応する処理関数を変更するだけです。
数日前、Anthony Ferrara は PHP の将来についていくつかの考えを書き留めました。私は彼の主張のほとんどに同意しますが、すべてではありません。この記事では、文字列や配列などのネイティブ型を、メソッド呼び出しを実行できる「疑似オブジェクト」として偽装するという特定の側面に焦点を当てます。
「疑似オブジェクト」が必要な理由を確認するために、いくつかの例から始めましょう:
<code>$str = "test foo bar";$str->length(); // == strlen($str) == 12$str->indexOf("foo") // == strpos($str, "foo") == 5$str->split(" ") // == explode(" ", $str) == ["test", "foo", "bar"]$str->slice(4, 3) // == substr($str, 4, 3) == "foo"$array = ["test", "foo", "bar"];$array->length() // == count($array) == 3$array->join(" ") // == implode(" ", $array) == "test foo bar"$array->slice(1, 2) // == array_slice($array, 1, 2) == ["foo", "bar"]$array->flip() // == array_flip($array) == ["test" => 0, "foo" => 1, "bar" => 2]</code>
ここで、$str
は単なる普通の文字列であり、$array
は単なる普通の文字列です。配列 - オブジェクトではありません。私たちがやったのは、メソッドを呼び出せるように、もう少しオブジェクトのようなプロパティを与えただけです。
上記の機能は手の届かないものではなく、すでに存在していることに注意してください。 PHP 拡張機能 scalar_objects を使用すると、ネイティブ PHP 型でオブジェクト指向メソッドを定義できます。
ネイティブ型へのオブジェクト スタイルの呼び出しメソッドの導入は、以下にリストする多くの利点ももたらします。
おそらくPHP に関する最も一般的な苦情は、標準ライブラリ内の関数の名前が一貫性がなく、不明瞭であること、およびパラメーターが一貫性がなく、順序が正しくないことです。いくつかの典型的な例:
<code>//不同的函数命名约定strposstr_replace//很不清晰的命名strcspnstrpbrk//颠倒的参数顺序strpos($haystack, $needle)array_search($needle, $haystack)</code>
これらの問題はしばしば強調されすぎますが (統合開発環境があります)、状況がかなり良好であることを否定するのは困難です。多くの関数には、奇妙な名前をはるかに超えた問題が存在することにも注意してください。多くの場合、エッジ ケースの動作は十分に考慮されていないため、呼び出し側のコードで特別な処理が必要になります。 (文字列関数の場合、エッジ ケースには通常、空の文字列や文字列の範囲外のオフセットが含まれます。)
一般的な提案は、関数の名前とパラメーターの順序を統一するために、PHP6 に膨大な数のエイリアスを実装することです。したがって、stringpos()
、stringreoplace()
、stringcomplement_span()
などになります。個人的には (これは多くの php-src 開発者の意見のようですが)、これらは私にとってほとんど意味がありません。これらの関数名は現在、PHP プログラマーの記憶に深く根付いているため、重要ではない表面上の変更を実装する価値がないように思えるかもしれません。
一方で、プリミティブ型の OO API の導入は、新しいパラダイムへの切り替えの副作用として API を再設計する機会を提供します)。これにより、古い API の予想される出力を考慮することなく、最初から開始できるようになります。 2 つの例:
$string->split($delimiter)
和$array->join($delimiter)
这样的方法,这些是函数功能是普遍接受的名字(与explode
和implode
不同)。另一方面如果有一个string\split($delimiter)
方法有这样的行为我会感到非常反感,因为已经存在的str_split
函数所做的是完全不同的(分组)。这是我实现原生类型面向对象API的主要动机:从零开始,允许我们实现一套合理的API设计。当然了,这个改动的所有好处不知这些。面向对象的语法提供了许多更深层的好处,将在下面讨论。
程序式的调用一般没有链式调用好。考虑下面的例子:
<code>$output = array_map(function($value) { return $value * 42;}, array_filter($input, function($value) { return $value > 10;});</code>
咋一看,哪个是arraay_map
和array_filter
各自的使用?(原文:what are array_map and array_filter applied to? )他们调用的顺序是什么?变量$input
隐藏在两个闭包之间,函数的书写顺序也和他们实际调用的顺序相反。现在同样的例子使用面向对象的语法:
<code>$output = $input->filter(function($value){ return $value > 10;})->map(function($value){ return $value * 42;});</code>
我敢说使用这种方式,操作的顺序(先filter
在map
)和初始输入数组$input
更加明显。
这个例子明显有人为拼凑的感觉,因为array_map
和array_filter
是函数参数顺序颠倒的另外一个例子(这就是为什么输入数组在中间)。再看另外一个输入参数在同一个位置的例子(来自实际的代码):
<code>substr(strstr(rtrim($className, '-'), '\\', '_'), 15);</code>
在这个例子中,最后面是一连串的额外的参数'_'), '\\', '_'), 15,
,很难把这些参数和应用的函数对应起来。把这个和使用面向对象方法的版本做个比较:
<code>$className->trimRight('_')->replace('\\', '_')->slice(15);</code>
这次函数运算和他们的参数紧密的联系在了一起,而且方法的调用和他们的执行顺序相匹配。
另一个来自这种语法的可读性的好处是needle/haystack
不明确问题。别名通过引入统一的参数顺序规范让我们解决了这个问题,使用面向对象的API这个问题基本不存在了。
<code>$string->contains($otherString);$string->contains($someValue);$string->indexOf($otherString);$string->indexOf($someValue);</code>
这里哪个部分应用了哪个规则的困惑不复存在了。
目前PHP有提供Contable
接口,这个接口可以通过类实现自定义的输出函数count($obj)
。为什么需要这个?因为我们PHP的函数没有多态。然而我们方法中确实需要多态:
如果数组实现$array->count()
作为一个方法,实际上代码是不会在意$array
是不是一个数组的,他可以是其他任何类型的实现count()
方法的对象,这基本上给了我们Countable
的所有行为,~(原文:This basically gives us the same behavior as Countable, just without the engine hackery it requires.)
这也是一个很一般的解决方案。举个例子,你可以实现一个实现所有字符串类型方法的UnicodeString
类,然后可以随便的使用正常的字符串和UnicodeStrings
。好吧,至少这还是理论。这很明显局限于那些字符串方法的使用,而且调用级联操作的时候会返回错误(原文:This would obviously only work as long as the usage is limited to just the string methods, and would fail once the concatenation operator is employed)(运算符重载目前只支持内核中的类)。
我仍然有强大的信念希望这个清晰起来,同样应用在数组等上面。通过继承相同的接口,你可有一个和数组行为方式相同的SplFixedArray
。(原文:you could have an SplFixedArray behave the same way as an array, by implementing the same interface.)
既然我们已经总结了这个方法的一些好处,让我们也来看看它的问题:
摘抄自Anthony发表的博客:
标量不是对象,但更重要的是他们不是任何类型。PHP依赖一个类型系统,字符串和数字是同一个。系统中许多的灵活性基于任何标量可以很容易的转换为其他标量。
更重要的是,由于松散的类型系统,你不可能在任何时候知道一个变量的类型是哪个。你可以说出你想要他是什么类型,但你不知道他内在的类型是什么。Even with casting or scalar type hinting it isn’t a perfect situation since there are cases where types can still change.
为了阐明这个问题,考虑下面的例子:
<code>$num = 123456789;$sumOfDigits = array_sum(str_split($num));</code>
这里$num
被作为一个字符串数字,被str_split
切分后使用array_sum
求和。现在试试同样效果的面向对象方法调用:
<code>$num = 123456789;$sumOfDigits = $num->chunk()->sum();</code>
这里字符串的cheunk()
方法被数字来调用。会发生什么??Anthony建议这样解决:
这意味着所有的标量运算将必然需要对应的标量类型。这将导致需要一个标量有所有的数学方法的对象模型,当然包括所有的字符串方法。真是一个噩梦。。。。。
就像引言中所说的那样,这绝不是一个可接受的解决方法。然而我想我们可以绝对侥幸的逃脱仅仅在那种情况的时候抛出一个错误(异常!)。为了解释为什么这种方法是可行的,让我们看看PHP可以拥有哪些类型。
除了对象之外,PHP有下面的变量类型:
<code>nullboolintfloatstringarrayresource</code>
现在,我们考虑下上面这些里面的哪些会需要面向对象的方法:我们首先去掉resource
,然后在剩下的里面看。null
和bool
明显不需要面向对象的方法,除非你想进行像$bool->invert()
这样无聊的转换。
绝大多数的数学函数使用面向对象的方法也不是很合适。考虑下面几个例子:
<code>log($n) $n->log()sqrt($n) $n->sqrt()acosh($n) $n->acosh()</code>
我想你会同意数学函数可读性比函数符号的形式更好。当然存在少许的面向对象方法你可以适当的应用数字类型,比如说$num->format(10)
读起来相当的不错。然而,关于这里,对于一个面向对象的数字API不是真正的需要,只有少量的函数你可能需要。(再者来说目前的数学API在命名方面没有太多的问题,而且数学操作相关的命名相当的标准。)
现在剩给我们的只有字符串和数组了,我们已经看到这两种类型有许多很棒的API。但关于松散类型的问题我们所有的必须要做的有哪些?下面是重要的几点:
我们经常性的把字符串视为数字的时候(例如来自HTTP或者DB),这样反过来是不对的:直接将数字作为字符串非常少见。举个栗子,下面的代码将让我们感到困惑:
<code>strpos(54321, 32, 1);</code>
这样将数字视为字符串是一个很怪异的操作,这种情况也ok啊,只需要强制转换一次就好了。使用原来的求和数字的例子:
<code>$num = 123456789;$sumOfDigits = ((string) $num)->chunk()->sum();</code>
现在我们弄明白了,是的,我们确实想将数字视为字符串。对我来说,这样来处理想这样使用这种技术的地方是可以接受的。
对于数组情况就更简单了:他不会出现讲一个数组操作视为一个其他不是数组类型的操作。
另一方面可以通过标量类型提示改善这个问题(我完全认为在PHP所有的版本都存在——最令人尴尬的问题是现在仍然没有(原文:which I totally assume to be present in any PHP version this gets in - really embarrassing that we still don’t have them))。如果内类型提示string
,你获取输入的字符串将会是一个字符串(即使传递给函数的不是——这取决于类型提示实现的具体内容)。
当然了,我并不是暗示这里没有一点问题。由于错误的函数设计,有时候可能会发生未知的类型潜入代码中,例如substr($str, strlen($str))
将自作聪明的返回bool(false)
而不是string(0) ""
。(不过,这个问题仅有substr
存在。面向对象的API不存在那个问题,所以你碰不到那个问题。)
除了若类型的问题之外,还有原生类型伪方法的一个语义的问题:PHP中的对象和其他类型相比有不同的传递语义(某种程度上和引用类似)。如果我们允许字符串和数组进行面向对象的方法调用,他们看起来会和对象很像,那样的话有些人可能期望他们有对象作为参数的传递语义。这个问题在在字符串和数组中都存在:
<code>function change($arg) { echo $arg->length(); //$arg looks like object $arg[0] = 'x'; //但是没有对象的传递语义}$str = 'foo';change($str); //$str stays the same$array = ['foo', 'o', 'o'];change($array); //$array stays the same</code>
我们当然将会改变传递语义。首先,在我看来通过值传递来传递像数组这种大的数据结构是一个相当low的想法,我更愿意他们像对象一样传递。然而,那将是一个相当大的突破性的向后兼容,并且那将不易于自动的重构(原文:However, that would be a pretty big backwards-compatibility break and one that’s not easy to refactor automatically)(至少我猜想是这样的。我没有做实验去探索这样一个改变带来的实际影响)。另一方面,对于字符串通过对象方式传递参数将是一个灾难,除非我们让字符串同一时间完全的不可变,放弃目前所有的局部变量的可变性(我个人发现非常的容易——去尝试改变一个Python字符串的一个字节)。
我不知道是否有好的方法去解决这个预期的问题,除了在我们的文档中强调字符串和数组在面向对象的方法中仅仅视作”伪对像“,不是真正的对象。
这个问题可以被扩展到其他的对象相关的特性。例如你可将会问像$string instanceof string
这样的是否正确。我还没有确定整个事情的完整走向。也许严格坚持仅仅在面向对象的方法使用,然后强调他们不是真正的对象会好一点。然而也许支持面向对象系统的更深层次的特性也会好点。这个观点应该进一步的思考下。
总而言之,这个方法有许多的问题,但我不认为他们特别重要。同时这个提供了一个很好的机会为我们的基本类型引入简洁明了的APIs,提高代码执行操作时候的可读性(可写性)。
那么这个想法目前的状态是什么呢?从我收集的内容来看,内部的人们不是特别的反对这个做法,但更愿意重命名所有的函数。主要的没有推进这个的原因是API提议~
为了这个目的,我创建了scalar_objects扩展,作为一个PHP扩展实现了这个功能。它允许你注册一个处理各自的原生类型的方法调用的类。看一个例子:
<code>class StringHandler { public function length(){ return strlen($this); } public function contains($str){ return false !== strpos($this, $str); }}register_primitive_type_handler('string', 'StringHandler');$str = 'foo bar baz';var_dump($str->legth()); //int(11)var_dump($str->contains('bar')); //bool(true)var_dump($str->contains('hello')); //bool(false)</code>
不久前,我开始了一个string handler包括一个API说明的工作,但一直没有真正的完成哪个项目(我希望我在不久找到一些重新开始他的动机)。当然也有许多其他项目在为实现这样的APIs而努力。
嗯,这是我想在PHP6中所看到的其中一个改进。我也许会为我的那个方向的计划写另外一篇文章。
原文链接 : http://nikic.github.io/2014/03/14/Methods-on-primitive-types-in-PHP.html
HOOKPHP : http://netsecurity.51cto.com/art/201407/446430.htm