首頁 >後端開發 >php教程 >PHP中原生類型的方法

PHP中原生類型的方法

WBOY
WBOY原創
2016-08-08 09:22:021474瀏覽

引言

第一次,翻譯別人的文章,用四級英語的水平來翻譯~~囧,可能有很多不太恰當的地方,儘管拍磚(有些地方實在想不到恰當的翻譯,我同時貼出了原文和自己很low的翻譯)。

翻譯這篇文章用了我3個晚上一個中午~,先弄清楚技術上大體再說什麼,然後在翻譯的~

這樣做的目的一方面鍛煉下自己的英文,一方面學習點國外的比較新的技術想法。

這篇文章主要講了對PHP中的原生類型實現物件導向的操作,透過擴展的方式實現,用來解決PHP中函數命名不規範、參數順序不規範、可讀性低的問題。

擴充的實作是透過改變ZEND引擎中呼叫物件導向方法時對應的處理函數來實現的,註冊一個函數,判斷物件導向呼叫者的類型,如果是IS_STRING則繼續自訂處理,否則傳回ZEND預設的處理函數,看看下面的解釋

HOOKPHP程式碼

PHP是解釋型語言,程式碼翻譯為中間字節碼由ZEND引擎解析執行。 PHP把中間字節碼稱之為OPCODE,每個OPCODE對應ZEND底層的一個處理函數,ZEND引擎最後執行這個處理函數。實作HOOK功能只需要改變HOOK OPCODE對應的處理函數即可。

PHP中原生類型的物件導向的方法

幾天前,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類型上面定義物件導向的方法。

原生類型引入物件式的呼叫方法也帶來了許多好處,我會在下面列出來:

一個讓API進一步讓簡潔的機會

聽到最多的最常見的關於PHP的抱怨很可能是標準庫中函數的前後矛盾、命名不清楚,還有前後不一致、順序混亂的參數。一些典型的例子:

<code>//不同的函数命名约定
strpos
str_replace

//很不清晰的命名
strcspn
strpbrk

//颠倒的参数顺序
strpos($haystack, $needle)
array_search($needle, $haystack)</code>

然而這些問題通常被過度強調了(我們有整合開發環境),很難否定狀況還是相當好的。還應該指出的是,許多函數表現出的問題遠遠超過了詭異的名字這個問題。通常邊緣情況的行為是沒有被完全考慮的,因而有了在呼叫程式碼中對它們進行特殊處理的需要。 (對字串函數來說,邊緣情況通常包括空字串或超出字串範圍的偏移量。)

一個一般的建議是在PHP6中實現巨大數量的別名,用來統一函數名稱和參數順序。因此我們將會有stringpos(),stringreoplace(),stringcomplement_span()或類似的。個人而言(而且這看起來像是對許多php-src開發者的意見)這些對我的意義很小。現在的這些函數名已經深深的根植在PHP程式設計師的記憶中了,對它們實現一些不重要的裝飾性的改變看起來好像不值得。

原生類型面向對象API的引入另一方面也提供了一個API重新設計的契機(原文:The introduction of an OO API for primitive types on the other hand offers an opportunity of an API redesign as a side effect of switching to a new paradigm)。這也是真正讓我們從零開始,不需要考慮舊的API的期望輸出。兩個例子:

  • 我很希望有$string->split($delimiter)$array->join($delimiter)這樣的方法,這些是函數功能是普遍接受的名字(與 explodeimplode不同)。另一方面如果有一個stringsplit($delimiter)方法有這樣的行為我會感到非常反感,因為已經存在的str_split函數所做的是完全不同的(分組)。
  • 我理所當然的喜歡新的對錯誤報告使用異常的API,由於這是一個面向對象的API,那是自動給定的,異常當然也是和重命名的API一起使用,然而這違背了現在所有的程式函數對錯誤處理使用警告的慣例,這不是一成不變的,但這確實是我想避免的一個爭議點:)

這是我實現原生類型物件導向API的主要動機:從零開始,允許我們實現一套合理的API設計。當然了,這個改動的所有好處不知這些。物件導向的語法提供了許多更深層的好處,將在下面討論。

提高了可读性

程序式的调用一般没有链式调用好。考虑下面的例子:

<code>$output = array_map(function($value) {
    return $value * 42;
}, array_filter($input, function($value) {
    return $value > 10;
});</code>

咋一看,哪个是arraay_maparray_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>

我敢说使用这种方式,操作的顺序(先filtermap)和初始输入数组$input更加明显。

这个例子明显有人为拼凑的感觉,因为array_maparray_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中的原生类型

除了对象之外,PHP有下面的变量类型:

<code>null
bool
int
float
string
array
resource</code>

现在,我们考虑下上面这些里面的哪些会需要面向对象的方法:我们首先去掉resource,然后在剩下的里面看。nullbool明显不需要面向对象的方法,除非你想进行像$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

以上就介绍了PHP中原生类型的方法,包括了方面的内容,希望对PHP教程有兴趣的朋友有所帮助。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn