日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

译注(3): NULL-计算机科学上最糟糕的失误

發(fā)布時(shí)間:2023/12/18 编程问答 41 豆豆
生活随笔 收集整理的這篇文章主要介紹了 译注(3): NULL-计算机科学上最糟糕的失误 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

原文:the worst mistake of computer science
注釋:有些術(shù)語不知道怎么翻譯,根據(jù)自己理解的意思翻譯了,如有不妥,敬請?zhí)岢?#xff1a;)
致謝: @vertextao @fracting

比windows反斜杠還丑,比===還古老,比PHP還常見,比跨域資源共享(CORS)還不幸,比Java泛型還令人失望,比XMLHttpRequest還不一致,比C語言的預(yù)處理器還讓人糊涂,比MongoDB還古怪,比UTF-16還令人遺憾。計(jì)算機(jī)科學(xué)里最糟糕的失誤在1965年被引入。(:可分別參考索引[1]-[9])

I call it my billion-dollar mistake…At that time, I was designing the first comprehensive type system for references in an object-oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
– Tony Hoare, inventor of ALGOL W.

為了紀(jì)念Hoare([10],[11],[13],[14],[15],[16],英國計(jì)算機(jī)科學(xué)家東尼·霍爾,霍爾邏輯的發(fā)明者,他還發(fā)明了并發(fā)理論Communicating Sequential Processes(CSP))的‘null’誕生50周年,這篇文章解釋了null是什么,為什么它是如此糟糕,以及如何正確解決它。

NULL錯(cuò)在哪?

最簡短的答案是:NULL是個(gè)沒有值的值,那便是問題所在。( The short answer: NULL is a value that is not a value. And that’s a problem. 感謝 @vertextao 對本句翻譯的推薦)

它已經(jīng)在最流行的編程語言中潰爛(festered)了,有各種叫法:NULL, nil, None, Nothing, Nil, nullptr等。每個(gè)編程語言里都有一些細(xì)微都差別。(:C/C++:NULL, Lua: nil, python:None, VB:Nothing, ObjectC:Nil, C++11: nullptr)

NULL帶來的問題,有些是在特定語言里才有的,有些則是普遍的,少數(shù)是同一個(gè)問題在不同語言里的不同表現(xiàn)。

NULL是:

  • 破壞類型(subverts types)
  • 草率的(is sloppy)
  • 特例(is a special case)
  • 使API捉襟見肘(makes poor APIs)
  • 加劇了不好的編程策略(exacerbates poor language decisions)
  • 難以調(diào)試(is difficult to debug)
  • 不可組合的(is non-composable)

1. NULL破壞類型(NULL subverts types)

靜態(tài)類型語言不需要執(zhí)行程序就可以檢查程序中類型的使用,從而對程序的行為提供一定程度的保證。

例如,在Java里面,我可以寫x.toUppercase(),編譯器就會(huì)檢查x的類型。如果x是個(gè)String類型,類型檢測就通過;如果x是個(gè)Socket類型,類型檢測就失敗。

靜態(tài)類型檢測在編寫大型、復(fù)雜軟件中十分有用。但是對于Java,這些漂亮的編譯時(shí)檢測有著致命的缺陷(suffer from a fatal flaw):任何引用都可能是個(gè)null,而且在一個(gè)null對象上調(diào)用方法會(huì)導(dǎo)致拋出NullPointerException異常。因此:

  • toUppercase可以被不是null的String對象安全地調(diào)用。
  • read()可以被不是null的InputStream對象安全地調(diào)用。
  • toString()可以被不是null的Object對象安全地調(diào)用。

Java并不是唯一犯錯(cuò)的編程語言。許多其他編程語言都有這個(gè)缺陷,當(dāng)然也包括了ALGOL語言。

在這些語言里,NULL默默地跳過了類型檢測,等到運(yùn)行時(shí)爆發(fā)各種NULL引用錯(cuò)誤,所有的類型都用NULL表示沒有這個(gè)語義。

2. NULL是草率的(is sloppy)

許多時(shí)候,使用null是沒有意義的。然而不幸的是,只要語言允許任意對象可以是NULL,那么任意對象就可能是NULL。

從而Java程序員可能會(huì)因?yàn)榭偸且獙懭缦碌拇a而患上腕管綜合癥。

if(str==null || str.equals("")){}

因?yàn)檫@個(gè)慣用法太常見,C#語言給String類型增加了String.IsNullOrEmpty方法:

if(string.IsNullOrEmpty(str)){}

真是令人憎惡。

Every time you write code that conflates null strings and empty strings, the Guava team weeps.
– Google Guava

說的好。但是當(dāng)你的類型系統(tǒng)(例如Java和C#)允許到處使用NULL,你就不能排除NULL的可能出現(xiàn),并且它一定會(huì)傳遞的到處都是。

Null的普遍存在導(dǎo)致了Java8增加了一個(gè)@NonNull修飾關(guān)鍵字讓類型系統(tǒng)有效地修正這個(gè)缺陷。

3. NULL是個(gè)特例(is a special-case)

由于NULL是一個(gè)沒有值的值,在許多情況下NULL變成了一個(gè)需要特別處理的地方。

指針(Pointers)

例如,考慮C++語言:

char c = 'A'; char *myChar = &c; std::cout<<*myChar<<std::endl;

myChar是一個(gè)char*類型,也就是一個(gè)指針,既指向char類型變量的內(nèi)存地址。編譯器會(huì)檢測它的類型,因此下面的代碼是無效的:

char *myChar = 123; // 編譯錯(cuò)誤 std::cout<< *myChar << std::endl;

由于123不能保證是一個(gè)char類型變量的地址,編譯器直接報(bào)錯(cuò)。但是如果我們把數(shù)字換成0(在C++里0代表NULL),那么編譯器就可以通過:

char *myChar = 0; std::cout << *myChar << std::endl; // 運(yùn)行時(shí)錯(cuò)誤

就像123一樣,NULL也不是一個(gè)有效的char變量地址,運(yùn)行時(shí)就報(bào)錯(cuò),但是由于0(NULL)是一個(gè)特例,編譯器通過了它。

字符串(Strings)

另一個(gè)特例是C語言的null結(jié)尾字符串。這個(gè)例子和其他例子有點(diǎn)不同,沒有指針或引用。但是同樣是由NULL是個(gè)沒有值的值這個(gè)做法導(dǎo)致的,在C語言的字符串里,0是一個(gè)不是字符(char)的字符(char)。

一個(gè)C風(fēng)格字符串是一串以0結(jié)尾的字節(jié)數(shù)組。例如:

因此,C風(fēng)格字符串里的字符可以是任意的256字節(jié),除了0(NULL 字符)。這導(dǎo)致了C風(fēng)格字符串的長度計(jì)算是O(n)的時(shí)間復(fù)雜度,更糟糕的是,C風(fēng)格字符串不能表示ASCII或者擴(kuò)展ASCII,而只能表示ASCIIZ。

:0和NULL是不同的,文章里的這個(gè)地方似乎沒有說明這點(diǎn),這個(gè)例子有待商榷,但不妨礙文章對NULL存在問題的分析。但是其實(shí)char* 只是一個(gè)容器,你可以往char* 數(shù)組里塞入任何編碼的字符串?dāng)?shù)據(jù),只要你解碼的時(shí)候能轉(zhuǎn)的回去就可以,例如你可以在里面塞入U(xiǎn)TF-8字符串,當(dāng)然這是計(jì)算機(jī)的另一面:任何數(shù)據(jù)的意義都取決于如何理解/解碼)

這個(gè)NULL字符特例,導(dǎo)致了許多問題:怪異的API,安全漏洞和緩存溢出。NULL是計(jì)算機(jī)科學(xué)里最糟糕的失誤,特別的,NULL結(jié)尾字符串是最糟糕的1字節(jié)擴(kuò)展失誤。

4. NULL使API捉襟見肘(makes poor APIs)

下一個(gè)例子里,我們考察下動(dòng)態(tài)語言的情況,你會(huì)看到在動(dòng)態(tài)語言里NULL依然被證明是個(gè)糟糕的失誤。

鍵值存儲(chǔ)(Key-value store)

假設(shè)我們在Ruby語言里創(chuàng)建了一個(gè)類用來做鍵值的存儲(chǔ)。例如一個(gè)緩存類,或者一個(gè)Key-value類型的數(shù)據(jù)庫存儲(chǔ)接口等。我們創(chuàng)建如下簡單的通用API:

class Store### associate key with value# def set(key, value)...end### get value associated with key, or return nil if there is no such key#def get(key)...end end

你可以想象下這個(gè)接口在其他語言里(Python、JavaScript、Java、C#等)的情況,大同小異。假設(shè)我們的程序里查找用戶的電話是一個(gè)很慢的資源密集型的方式,有可能訪問了一個(gè)web service來查找。為了提高性能,我們會(huì)使用Store來做緩存,使用用戶名字做鍵,用戶電話做值。

store = Store.new() store.set('Bob', '801-555-5555') store.get('Bob') # returns '801-555-5555', which is Bob’s number store.get('Alice') # returns nil, since it does not have Alice

但是現(xiàn)在get接口的返回值產(chǎn)生了二義性!它可能意味著:

  • 緩存里不存在該用戶,例如Alice。
  • 緩存里存在該用戶,但是該用戶沒有電話號(hào)碼。
  • 一種情況下需要耗時(shí)的重新計(jì)算,另一種情況下則是秒回。但是我們的程序并沒有足夠充分地區(qū)分這兩種情況。在實(shí)際的代碼里,這種情況經(jīng)常出現(xiàn),以一種復(fù)雜而微妙的方式呈現(xiàn),并不容易直接識(shí)別。從而,本來簡潔通用的API需要做各種特殊情況的處理,而增加了代碼的繁雜。

    雙重麻煩

    JavaScript語言有同樣的問題,而且對于每個(gè)對象都存在該問題。如果一個(gè)對象的屬性(property)不存在,JavaScript返回了一個(gè)值來表示,JavaScript的設(shè)計(jì)者可以選擇使用null來表示。

    但是他們擔(dān)心屬性可能是存在,但是值被設(shè)置為了null。糟糕的是,JavaScript增加了一個(gè)undefined對象來區(qū)分null屬性和不存在兩種情況。

    但是如果一個(gè)屬性是存在的,可是被設(shè)置為undefined了呢?JavaScript沒有考慮這點(diǎn)。實(shí)際上你沒辦法區(qū)分屬性不存在和屬性是undefined。

    因此,JavaScript應(yīng)該只使用一個(gè),而不是造出了兩個(gè)不同的NULL。

    :事實(shí)上,許多JavaScript編程規(guī)范也建議只用xx==null和xx!=null來比較一個(gè)值是null或undefined,而不建議使用===做與null和undefined的比較,其實(shí)就是只把它們當(dāng)作一個(gè)NULL來看待)

    5. NULL加劇了不好的編程策略(exacerbates poor language decisions)

    Java語言會(huì)默默地在引用類型(reference types)和基本類型(Primitive types)之間做轉(zhuǎn)換(裝箱和拆箱),這使得問題變得更怪異。

    例如,下面的代碼無法通過編譯:

    int x = null; // compile error

    但是,下面的代碼可以通過編譯,但是運(yùn)行時(shí)卻會(huì)拋出NullPointerException:

    Integer i = null; int x = i; // runtime error

    成員方法可以被null調(diào)用已經(jīng)夠糟糕了,更糟的是你根本沒看見成員方法被調(diào)用。

    6. NULL難以調(diào)試(difficult to debug)

    C++語言是NULL的重災(zāi)區(qū)。在NULL指針上調(diào)用一個(gè)方法甚至不會(huì)導(dǎo)致程序的立刻崩潰,而是:它可能會(huì)導(dǎo)致程序崩潰。

    #include <iostream> struct Foo {int x;void bar() {std::cout << "La la la" << std::endl;}void baz() {std::cout << x << std::endl;} }; int main() {Foo *foo = NULL;foo->bar(); // okayfoo->baz(); // crash }

    如果使用GCC編譯上述代碼,第一個(gè)調(diào)用會(huì)成功,而第二個(gè)調(diào)用會(huì)崩潰。為什么呢?這是因?yàn)閒oo->bar()的值編譯期可以確定,所以編譯器直接繞過了運(yùn)行時(shí)查找vtable,轉(zhuǎn)成了調(diào)用一個(gè)靜態(tài)的方法Foo_bar(foo),并且把this作為第1個(gè)參數(shù)傳遞進(jìn)去。由于bar方法里并沒有對NULL指針做解引用(dereference)動(dòng)作,因此不會(huì)崩潰。然而baz就沒這么幸運(yùn)了,直接導(dǎo)致了segmentation fault。

    但是假設(shè),我們讓bar成為一個(gè)virtual方法,意味著它可能被子類覆蓋。

    ...virtual void bar() { ...

    作為一個(gè)虛函數(shù),foo->bar()需要在運(yùn)行時(shí)對vtable做查找,以確認(rèn)bar()方法是否被子類覆蓋。而由于foo是個(gè)NULL指針,當(dāng)調(diào)用foo->bar()的時(shí)候,程序就會(huì)因?yàn)閷ULL做解引用而崩潰。

    int main() {Foo *foo = NULL;foo->bar(); // crashfoo->baz(); }

    NULL讓調(diào)試變得十分不直觀,讓調(diào)試變得十分困難。準(zhǔn)確地說,對NULL指針做解引用是一個(gè)未定義的C++行為(C++標(biāo)準(zhǔn)并沒有規(guī)定),所以不同的編譯器(平臺(tái)、版本)都可能有不同的做法,技術(shù)上來說你根本不知道會(huì)發(fā)生什么。再一次,在實(shí)際的程序里,這種情況往往隱藏在復(fù)雜的代碼里,而不是如上面代碼那樣直接可以觀察到。

    7.NULL帶來不可組合(non-composable)

    編程語言是構(gòu)建在組合的基礎(chǔ)上:在一個(gè)抽象層上使用另一個(gè)抽象層的能力。這可能是唯一的對所有編程語言(programing language)、類庫(library)、框架(framework)、范式(paradigm)、API來說都重要的特性(feature)。

    :有一句話說“任何一個(gè)軟件問題都可以通過添加一個(gè)抽象層解決”,但是這個(gè)說法不是萬能的,例如文章作者吐槽的Java泛型就是一個(gè)例子,底層不修改,只通過擦除的方式支持泛型,在運(yùn)行期就會(huì)丟失泛型信息,參考[6])

    事實(shí)上,組合性是許多問題背后的根本問題。但是,像上面的Store類的API,返回nil既可能是用戶不存在,也可能是用戶存在但沒有電話號(hào)碼,就不具有可組合性。

    C#添加了一些語法特性來解決NULL帶來的問題。例如,Nullable<T>。你可以使用“可空”(nullable)類型。示例代碼如下:

    int a = 1; // integer int? b = 2; // optional integer that exists int? c = null; // optional integer that does not exist

    但是Nullable里面的T只能是非可空類型,這并不能更好的解決Store的問題。例如

  • string一開始是一個(gè)可空類型,你就不能讓string變成非可空類型。
  • 即使string是一個(gè)非可空類型,從而string?是可空類型。你仍然不能區(qū)分這種情況,是否有string??。
  • :C#實(shí)際上已經(jīng)提供了解決方案。)

    解決方案(The solution)

    NULL到處都是,從低級(jí)語言到高級(jí)語言里都有。以至于大家默認(rèn)假設(shè)NULL是必要的,就像整型運(yùn)算、或者I/O一樣。

    然而并非如此!你可以使用一個(gè)完全沒有NULL的語言。問題的根本在于NULL是表示沒有值的值(non-value value),作為一個(gè)哨兵,作為一個(gè)特殊例子,蔓延到到處。

    我們需要一個(gè)包含信息的實(shí)體,它應(yīng)該具備:

  • 能確定里面是否含有值。
  • 如果有值,可以包含任意類型。這正是Haskel的Maybe,Java的Optional,以及Swift的Optional等類型。
  • 例如,在Scala語言里,Some[T]持有一個(gè)類型為T的值。None持有“沒有值”。它們都是Option[T]的自類型:

    對于不熟悉Maybe/Options類型的讀者來說,可能認(rèn)為這換湯不換藥,只是從一種垃圾(NULL類型)轉(zhuǎn)成了另一種垃圾(NULL類型)。然而它們之間有著細(xì)微而關(guān)鍵的不同。

    在一個(gè)靜態(tài)語言里,你無法用None代替任意類型繞過類型系統(tǒng)。None只能在我們確實(shí)需要一個(gè)Option類型的地方使用。Option被類型系統(tǒng)顯式化了。

    在一個(gè)動(dòng)態(tài)語言里,你不能混淆Maybe/Option和一個(gè)含有值的類型。

    讓我們回到最開始的Store類,但是這次我們假設(shè)ruby被升級(jí)為了“ruby-possibly”語言。如果值存在,Store類會(huì)返回了Some類型,而如果值不存在,會(huì)返回None類型。對于電話號(hào)碼這個(gè)例子,Some被用來表示一個(gè)電話號(hào)碼,None被用來表示沒有電話號(hào)碼。因此,存在兩層的“存在/不存在”表示:

  • 外層的Maybe表示用戶是否存在。
  • 內(nèi)層的Maybe表示存在的用戶是否含有電話號(hào)碼。
  • cache = Store.new() cache.set('Bob', Some('801-555-5555')) cache.set('Tom', None())bob_phone = cache.get('Bob') bob_phone.is_some # true, Bob is in cache bob_phone.get.is_some # true, Bob has a phone number bob_phone.get.get # '801-555-5555'alice_phone = cache.get('Alice') alice_phone.is_some # false, Alice is not in cachetom_phone = cache.get('Tom') tom_phone.is_some # true, Tom is in cache tom_phone.get.is_some #false, Tom does not have a phone number

    最根本的區(qū)別是,“不存在”和“值是垃圾”之間不再混合在一起。

    維護(hù)Maybe/Option

    讓我們繼續(xù)展示更多的non-NULL代碼。假設(shè)在Java8+,我們有一個(gè)整數(shù)可能存在或不存在,如果存在,我們就把它打印出來。

    Optional<Integer> option = ... if (option.isPresent()) {doubled = System.out.println(option.get()); }

    這個(gè)代碼已經(jīng)解決了問題,但是許多Maybe/Option的實(shí)現(xiàn),提供了更好的函數(shù)式方案,例如Java:

    option.ifPresent(x -> System.out.println(x)); // or option.ifPresent(System.out::println)

    代碼更短只是一個(gè)方面,更重要的是這更安全一些。記住如果一個(gè)值不存在,那么option.get()會(huì)拋出錯(cuò)誤。前面的例子里,get()方法的調(diào)用在一個(gè)if判斷語句的保護(hù)范圍內(nèi)。而在這個(gè)例子里,ifPresent()是get()調(diào)用的保證。這個(gè)代碼明顯沒有BUG,這比沒有明顯的BUG好很多。(It makes there obviously be no bug, rather than no obvious bugs.)

    Options可以被看作是一個(gè)長度為1的容器。例如,我們可以讓有值的時(shí)候放大兩倍,沒值的時(shí)候保持為空:

    option.map(x -> 2 * x);

    我們也可以在option對象上做一個(gè)操作,讓它返回一個(gè)option對象,然后再壓扁它。(:也就把Option<Option<T>>壓扁成Option<T>)

    option.flatMap(x -> methodReturningOptional(x));

    我們可以為option提供一個(gè)默認(rèn)值,如果它不存在的話:

    option.orElseGet(5);

    小結(jié)一下,Maybe/Option的價(jià)值在于:

  • 減少了對值存在和不存在假設(shè)的風(fēng)險(xiǎn)。(:if語句很容易被程序員漏掉)
  • 使得在option類型的數(shù)據(jù)上的操作簡單而又安全。
  • 顯式地聲明任意不安全的存在性假設(shè)(例如,使用.get()方法)。
  • Down with NULL!

    NULL的糟糕設(shè)計(jì)在持續(xù)的造成編寫代碼的痛點(diǎn)。只有一些語言提供了正確的解決方案來避免錯(cuò)誤。如果你必須選擇一個(gè)含有NULL的語言,至少你應(yīng)該理解這些缺點(diǎn),并使用Maybe/Option等價(jià)的策略。

    下面是NULL/Maybe在不同語言里的支持得分情況

    :C#實(shí)際得分應(yīng)該更高,文章后有評(píng)論提到“C# should have 4 stars as it has support for your proposed solution (since .NET version 2.0… which came out in 2005) via the Nullable struct.”)
    : 這個(gè)圖里沒有包括最新的TypeScript,TypeScript的設(shè)計(jì)者和C#的設(shè)計(jì)者都是 Anders Hejlsberg )

    評(píng)分規(guī)則如下:

    什么時(shí)候NULL是合適的(When is NULL okay)

    在少數(shù)特殊的情況下,0和NULL在減少CPU周期,改進(jìn)性能方面,是有用的。例如在C語言里,有用的0和NULL應(yīng)該被保留。

    真正的問題

    NULL背后反應(yīng)的本質(zhì)問題是:一個(gè)同樣的值含有兩種或多種不同的語義,例如indexOf返回-1,NUL終結(jié)的C風(fēng)格string是另一個(gè)例子。

    :但是其實(shí)數(shù)據(jù)本身是沒有意義的,程序如何解釋數(shù)據(jù),不僅僅依靠類型,只是說如果類型沒有提供好的內(nèi)置支持,痛點(diǎn)總是存在和更容易傳播,參考破窗效應(yīng)[12]。)

    :沒有Maybe的時(shí)候,文章中的例子,解決二義性問題當(dāng)然可以用不同錯(cuò)誤碼解決,但是null問題無處不在,每個(gè)case你都要面對,不信查查你的代碼。)

    references

    :我根據(jù)需要,補(bǔ)充了這些資料,也都很有意思,可點(diǎn)開進(jìn)一步閱讀。)

    [1] Why Windows Uses Backslashes and Everything Else Uses Forward Slashes
    [2] Why is the DOS path character "/"?
    [3] JavaScript equality game
    [4] Why does PHP suck?
    [5] wiki:CORS
    [6] Java Generics Suck
    [7] MDN:XMLHttpRequest
    [8] GCC:Macro
    [9] wiki:UTF-16
    [10] wiki:Tony Hoare
    [11] wiki-zh-cn: Tony Hoare
    [12] wiki: Broken windows theory(破窗效應(yīng))
    [13] wiki: Hoare logic
    [14] wiki-zh-cn: Hoare logic
    [15] Communicating Sequential Processes(CSP)
    [16] A Conversation with Sr. Tony Hoare

    轉(zhuǎn)載于:https://www.cnblogs.com/math/p/null.html

    總結(jié)

    以上是生活随笔為你收集整理的译注(3): NULL-计算机科学上最糟糕的失误的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。