1. 程式人生 > >如何設計一門語言(十二)——設計可擴充套件的型別

如何設計一門語言(十二)——設計可擴充套件的型別

在思考怎麼寫這一篇文章的時候,我又想到了以前討論正交概念的事情。如果一個系統被設計成正交的,他的功能擴充套件起來也可以很容易的保持質量這是沒錯的,但是對於每一個單獨給他擴充套件功能的個體來說,這個系統一點都不好用。所以我覺得現在的語言被設計成這樣也是有那麼點道理的。就算是設計Java的那誰,他也不是傻逼,那為什麼Java會被設計成這樣?我覺得這跟他剛開始想讓金字塔的底層程式設計師也可以順利使用Java是有關係的。

難道好用的語言就活該不好擴充套件碼?實際上不是這樣的,但是這仍然是上面那個正交概念的問題。一個容易擴充套件的語言要讓你覺得好用,首先你要投入時間來學習他。如果你想簡單的借鑑那些不好擴充套件的語言的經驗(如Java)來在短時間內學會如何使用一個容易擴充套件的語言(如C++/C#)——你的出發點就已經投機了。所以這裡有一個前提值得我再強調一次——首先你需要投入時間去學習他。

正如我一直在群裡說的:"C++需要不斷的練習——vczh"。要如何練習才能讓自己藉助語言做出一個可擴充套件的架構呢?先決條件就是,當你在練習的時候,你必須是在練習如何實現一個從功能上就要求你必須保證他的可擴充套件性的系統,舉個例子,GUI庫就是其中的一類。我至今認為,學會實現一個GUI庫,比通過練習別的什麼東西來提高自己的能力來講,簡直就算一個捷徑了。

那麼什麼是擴充套件呢?簡單的來講,擴充套件就是在不修改原有程式碼的情況下,僅僅通過新增新的程式碼,就可以讓原有的功能適應更多的情況。一般來講,擴充套件的主要目的並不是要增加新的功能,而是要只增加新程式碼的前提下修改原有的功能。譬如說原來你的系統只支援SQLServer,結果有一天你遇到了一個喜歡Oracle的新客戶,你要把東西賣給他,那就得支援Oracle了吧。但是我們知道,SQLServer和Oracle在各種協議(asp.net、odbc什麼的)上面是有偏好的,用DB不喜歡的協議來連線他的時候bug特別多,這就造成了你又可能沒辦法使用單一的協議來正確的使用各種資料庫,因此擴充套件的這個擔子就落在你的身上了。當然這種系統並不是人人都要寫,我也可以換一個例子,假如你在設計一個GPU叢集上的程式,那麼這個叢集的基礎架構得支援NVidia和AMD的顯示卡,還得支援DirectCompute、Cuda和OpenCL。然而我們知道,OpenCL在不同的平臺上,有互不相容的不同的bug,導致你實際上並不可能僅僅通過一份不變的程式碼,就充分發揮OpenCL在每一個平臺上的最佳狀態

……現實世界的需求真是orz(OpenCL在windows上用AMD卡定義一個struct都很容易導致崩潰什麼的,我覺得這根本不能用)……

在語言裡面談擴充套件,始終都離不開兩個方面:編譯期和執行期。這些東西都是用看起來很像pattern matching的方法組織起來的。如果在語言的型別系統的幫助下,我們可以輕鬆做出這樣子的架構,那這個語言就算有可擴充套件的型別了。

  1. 編譯期對型別的擴充套件

這個其實已經被人在C++和各種靜態型別的函式式語言裡面做爛了。簡單的來講,C++處理這種問題的方法就是提供偏特化。可惜C++的偏特化只讓做在class上面,結果因為大家對class的誤解很深,順便連偏特化這種比OO簡單一萬倍的東西也誤解了。偏特化不允許用在函式上,因為函式已經有了過載,但是C++的各種標準在使用函式來擴充套件型別的時候,實際上還是當他是偏特化那麼用的。我舉個例子。

C++11多了一個foreach迴圈,寫成for(auto x : xs) { … }。STL的型別都支援這種新的for迴圈。C++11的for迴圈是為了STL的容器設計的嗎?顯然不是。你也可以給你自己寫的容器加上for迴圈。方法有兩種,分別是:1、給你的型別T加上T::begin和T::end兩個成員函式;2、給你的型別T實現begin(T)和end(T)兩個全域性函式。我還沒有去詳細考證,但是我認為預設的begin(T)和end(T)全域性函式就是去呼叫T::begin和T::end的,因此for迴圈只需要認begin和end兩個全域性函式就可以了。

那自己的型別怎麼辦呢?當然也要去過載begin和end了。現在全域性函式沒有過載,因此寫出來大概是:
template<typename T> auto begin(const T& t)->decltype(t.begin()) { return t.begin(); }

template<typename T> my_iterator<T> begin(const my_container<T>& t);

template<typename T> my_range_iterator<T> begin(pair<T, T> range);

如果C++的函式支援偏特化的話,那麼上面這段程式碼就會被改成這樣,而且for迴圈也就不去找各種各樣的begin函數了,而只認定那一個std::begin就可以了:
template<typename T> auto begin(const T& t)->decltype(t.begin()) { return t.begin(); }

template<typename T> my_iterator<T> begin< my_container<T>>(const my_container<T>& t);

template<typename T> my_range_iterator<T> begin< pair<T, T>>( const pair<T, T>& range);

為什麼要偏特化呢?因為這至少保證你寫出來的begin函式跟for函式想要的begin函式的begin函式的簽名是相容的(譬如說不能有兩個引數之類的)。事實上C++11的for迴圈剛開始是要求大家通過偏特化一個叫做std::range的型別來支援的,這個range型別裡面有兩個static函式,分別叫begin和end。後來之所以改成這樣,我猜大概是因為C++的每一個函式過載也可以是模板函式,因此就不需要引入一個新的型別了,就讓大家去過載好了。而且for做出來的時候,C++標準裡面還沒有concept,因此也沒辦法表達"對於所有可以迴圈的型別T,我們都有std::range<T>必須滿足這個叫做range_loopable<T>的concept"這樣的前置條件。

過載用起來很容易讓人走火入門,很多人到最後都會把一些僅僅看起來像而實際上語義完全不同的東西用過載來表達,函式的引數連相似性都沒有。其實這是不對的,這種時候就應該把函式改成兩個不同的名字。假如當初設計C++的是我,那我一定會把函式過載幹掉,然後允許人們對函式進行偏特化,並且加上concept。既然std::begin已經被定義為迴圈的輔助函數了,那麼你過載一個std::begin,他卻不能用來迴圈(譬如說有兩個引數什麼的),那有意義嗎?完全沒有。

這種例子還有很多,譬如如何讓自己的型別可以被<<到wcout的做法啦,boost的那個serialization框架,還有各種各樣的庫,其實都利用了相同的思想——對型別做編譯期的擴充套件,使用一些手段使得在不需要修改原來的程式碼的前提下,就可以讓編譯器找到你新加進去的函式,從而使得呼叫的寫法不用發生變化就可以對原有的功能支援更多的情況。至少我們讓我們自己的型別支援for迴圈就不需要翻開std::begin的程式碼把我們的型別寫進去,只需要在隨便什麼空白的地方過載一個std::begin就可以了。這就是一個很好地體現。C++的標準庫一直在引導大家正確設計一個可擴充套件的架構,可惜很多人都意識不到這一點,為了自己那一點連正確性都談不上的強迫症,放棄了很多東西。

很多靜態型別的函式式語言使用concept來完成上述的工作。當一個concept定義好了之後,我們就可以通過對concept的實現進行偏特化來讓我們的型別T滿足concept的要求,來讓那些呼叫這個concept的泛型程式碼,可以在處理的物件是T的時候,轉而呼叫我們提供的實現。Haskell就是一個典型的例子,一個sort函式必然要求元素是可比較的,一個可以比較的型別定義為實現了Ord這個type class的型別。所以你只要給你自己的型別T實現Ord這個type class,那sort函式就可以對T的列表進行排序了。

對於C++和C#這種沒有concept或者concept不是主要概念的語言裡面,對型別做靜態的擴充套件只需要你的型別滿足"我可以這麼這麼幹"就可以了。譬如說你過載一個begin和end,那你的型別就可以被foreach;你給你的型別實現了operator<等函式,那麼一個包含你的型別的容器就可以被sort;或者C#的只要你的型別T<U>有一大堆長得跟System.Linq.Enumerable裡面定義的擴充套件函式一樣的擴充套件函式,那麼Linq的神奇的語法就可以用在你的型別上等等。這跟動態型別的"只要它長的像鴨子,那麼它就是鴨子"的做法有異曲同工之效。如果你的begin函式的簽名沒寫對,編譯器也不會屌你,直到你對他for的時候編譯器才會告訴你說你做錯了。這跟很多動態型別的語言的很多錯誤必須在執行的時候才發現的性質也是類似的。

Concept對於可靜態擴充套件的型別的約束,就如同型別對於邏輯的約束一樣。沒有concept的C++模板,就跟用動態型別語言寫邏輯一樣,只有到用到的那一刻你才知道你到底寫對了沒有,而且錯誤也會爆發在你使用它的地方,而不是你定義它的地方。因此本著編譯器幫你找到儘可能多的錯誤的原則,C++也開始有concept了。

C#的擴充套件方法用在Linq上面,其實編譯器也要求你滿足一個內在的concept,只是這個概念無法用C#的語法表達出來。所以我們在寫Linq Provider的時候也會有同樣的感覺。Java的interface都可以寫預設實現了,但是卻沒有靜態方法。這就造成了我們實際上無法跟C++和C#一樣,在不修改原有程式碼的前提下,讓原有的功能滿足更多的情況。因為C#的新增擴充套件方法的情況,到了Java裡面就變成讓一個類多繼承自一個interface,必須修改程式碼了。Java的這個功能特別的雞肋,不知道是不是他故意想跟C#不一樣才設計成這個樣子的,可惜精華沒有抄去,卻抄了糟粕。

  1. 執行期對型別的擴充套件

自從Java吧靜態型別和麵向物件捆綁在一起之後,業界對"執行期對型別的擴充套件"這個主題思考了很多年,甚至還出了一本著作叫《設計模式》,讓很多人捧為經典。大家爭先恐後的學習,而效果卻不怎麼樣。這是因為《設計模式》不好嗎?不是。這是因為靜態型別和麵向物件捆綁在一起之後,設計一個可擴充套件的架構就很難嗎?也不是。真正的原因是,Java設計(好像也是抄的Simular?我記不太清楚了)的虛擬函式把這個問題的難題提升了一個等級。

用正確的概念來理解問題可以讓我們更容易的掌握問題的本質。語言是有魔力的,習慣說中文的人,思考方式都跟中國人差不多。習慣說英語的人,思考方式都跟美國人差不多。因此習慣了使用C++/C#/Java的人,他們對於面向物件的想法其實也是差不多的。這是人類的天性。儘管大家鼓吹說語言只是工具,我們應該掌握方法論什麼的,但是這就跟要求男人面對一個萌妹紙不勃起一樣,違背了人類的本性,難度簡直太高了。於是我今天從虛擬函式和Visitor模式講起,告訴大家為什麼虛擬函式的這種形式會讓"擴充套件的時候不修改原有的程式碼"變難。

絕大多數的系統的擴充套件,都可以最後化簡(這並不要求你非得這麼做)為"當它的型別是這個的時候你就幹那個"的這麼件事。對於在編譯的時候就已經知道的,我們可以用偏特化的方法讓編譯器在生成程式碼的時候就先搞好。對於執行的時候,你拿到一個基類(其實為什麼一定要有基類?應該有的是interface!參見上一篇文章——刪減語言的功能),那如何O(1)時間複雜度(這裡的n指的是所有跟這次跳轉有關係的型別的數量)就跳轉到你想要的那個分支上去呢?於是我們有了虛擬函式。

靜態的擴充套件用的是靜態的分派,於是編譯器幫我們把函式名都hardcode到生成的程式碼裡面。動態的型別用的是動態的分派,於是我們得到的當然是一個相當於函式指標的東西。於是我們會把這個函式指標儲存在從基類物件可以O(1)訪問到的地方。虛擬函式就是這麼實現的,而且這種型別的分派必須要這麼實現的。但是,寫成程式碼就一定要寫程式函式嗎

其實本來沒什麼理由讓一個語言(或者library)長的樣子必須有提示你他是怎麼實現的功能。關心太多容易得病,執著太多心生痛苦啊。所以好好的解決問題就好了。至於原理是什麼,下了班再去關心。估計還有一些人不明白為什麼不好,我就舉一個通俗的例子。我們都知道dynamic_cast的效能不怎麼樣,虛擬函式用來做if的效能要遠遠比dynamic_cast用來做if的效能好得多。因此下面所有的答案都基於這個前提——要快,不要dynamic_cast!

  1. 處理HTML

好了,現在我們的任務是,拿到一個HTML,然後要對他做一些功能,譬如說把它格式化成文字啦,看一下他是否包含超連結啦等等。假設我們已經解決HTML的語法分析問題,那麼我們會得到一顆靜態型別的語法樹。這棵語法樹如無意外一定是長下面這個樣子的。另外一種選擇是存成動態型別的,但是這跟面向物件無關,所以就不提了。

class DomBase

{

public:

    virtual ~DomBase();

    static shared_ptr<DomBase> Parse(const wstring& htmlText);

};

class DomText : public DomBase{};

class DomImg : public DomBase{};

class DomA : public DomBase{};

class DomDiv : public DomBase{};

......

HTML的tag種類繁多,大概有那麼上百個吧。那現在我們要給他加上一個格式化成字串的功能,這顯然是一個遞迴的演算法,先把sub tree一個一個格式化,最後組合起來就好了。可能對於不同的非文字標籤會有不同的格式化方法。程式碼寫出來就是這樣——基本上是唯一的作法:

class DomBase

{

public:

    virtual ~DomBase();

    static shared_ptr<DomBase> Parse(const wstring& htmlText);

    virtual voidFormatToText(ostream& o); // 預設實現,把所有subtree的結果合併

};

class DomText : public DomBase

{

public:

    voidFormatToText(ostream& o); // 直接輸出文字

};

class DomImg : public DomBase

{

public:

    voidFormatToText(ostream& o); // 輸出imgtag內容

};

// 其它實現略

class DomA : public DomBase{};

class DomDiv : public DomBase{};

這已經構成一個基本的HTML的Dom Tree了。現在我提一個要求如下,要求在不修改原有程式碼只新增新程式碼的情況下,避免dynamic_cast,實現一個考察一顆Dom Tree是否包含超連結的功能。能做嗎?

無論大家如何苦思冥想,答案都是做不到。儘管這麼一看可能覺得這不是什麼大事,但實際上這意味著:你無法通過新增模組的方式來給一個已知的Dom Tree新增"判斷它是否包含超連結"的這個功能。有的人可能會說,那把它建模成動態型別的樹不就可以了?這是沒錯,但這實際上有兩個問題。第一個是著顯著的增加了你的測試成本,不過對於充滿了廉價勞動力的web行業來說這好像也不是什麼大問題。第二個更加本質——HTML可以這麼做,並不代表所有的東西都可以裝怎麼做事吧。

那在靜態型別的前提下,要如何解決這個問題呢?很久以前我們的《設計模式》就給我們提供了visitor模式,用來解決這樣的問題。如果把這個Dom Tree修改成visitor模式的程式碼的話,那原來FormatToText就會變成這個樣子:

class DomText;

class DomImg;

class DomA;

class DomDiv;

class DomBase

{

public:

    virtual ~DomBase();

    static shared_ptr<DomBase> Parse(const wstring& htmlText);

    class IVisitor

    {

    public:

        virtual ~IVisitor();

        virtual void Visit(DomText* dom) = 0;

        virtual void Visit(DomImg* dom) = 0;

        virtual void Visit(DomA* dom) = 0;

        virtual void Visit(DomDiv* dom) = 0;

    };

    virtual void Accept(IVisitor* visitor) = 0;

};

class DomText : public DomBase

{

public:

    void Accept(IVisitor* visitor)override

    {

        visitor->Visit(this);

    }

};

class DomImg : public DomBase

{

public:

    void Accept(IVisitor* visitor)override

    {

        visitor->Visit(this);

    }

};

class DomA : public DomBase

{

public:

    void Accept(IVisitor* visitor)override

    {

        visitor->Visit(this);

    }

};

class DomDiv : public DomBase

{

public:

    void Accept(IVisitor* visitor)override

    {

        visitor->Visit(this);

    }

};

class FormatToTextVisitor : public DomBase::IVisitor

{

private:

    ostream& o;

public:

    FormatToTextVisitor(ostream& _o)

        :o(_o)

    {

    }

    void Visit(DomText* dom){} // 直接輸出文字

    void Visit(DomImg* dom){} // 輸出imgtag內容

    void Visit(DomA* dom){} // 預設實現,把所有subtree的結果合併

    void Visit(DomDiv* dom){} // 預設實現,把所有subtree的結果合併

    static void Evaluate(DomBase* dom, ostream& o)

    {

        FormatToTextVisitor visitor(o);

        dom->Accept(&visitor);

    }

};

看起來長了不少,但是我們驚奇地發現,這下子我們可以通過提供一個Visitor,來在不修改原有程式碼的前提下,避免dynamic_cast,實現判斷一顆Dom Tree是否包含超連結的功能了!不過別高興得太早。這兩種做法都是有缺陷的。

虛擬函式的好處是你可以在不修改原有程式碼的前提下新增新的Dom型別,但是所有針對Dom Tree的操作緊密的耦合在了一起,並且邏輯還分散在了每一個具體的Dom型別裡面。你新增一個新功能就要修改所有的DomBase的子類,因為你要給他們都新增你需要的虛擬函式。

Visitor的好處是你可以在不修改原有程式碼的前提下新增新的Dom操作,但是所有的Dom型別卻緊密的耦合在了一起,因為IVisitor型別要包含所有DomBase的子類。你每天加一個新的Dom型別就得修改所有的操作——即IVisitor的介面和所有的具體的Visitor。而且還有另一個問題,就是虛擬函式的預設實現寫起來比較鳥了

所以這兩種做法都各有各的耦合。

  1. 碰撞系統

看了上面對於虛擬函式和Visitor的描述,大家大概知道了虛擬函式和Visitor其實都是同一個東西,只是各有各的犧牲。因此他們是可以互相轉換的——大家通過不斷地練習就可以知道如何把一個解法表達成虛擬函式的同時也可以表達成Visitor了。但是Visitor的程式碼又臭又長,所以下面我只用虛擬函式來寫,懶得敲太多程式碼了。

虛擬函式只有一個this引數,所以他是single dynamic dispatch。對於碰撞系統來說,不同種類的物體之間的碰撞程式碼都是不一樣的,所以他有兩個"this引數",所以他是multiple dynamic dispatch。在接下來的描述會發現,只要遇上了multiple dynamic dispatch,在現有的架構下避免dynamic_cast,無論你用虛擬函式還是visitor模式,做出來的solution全都是不管操作有沒有偶合在一起,反正型別是肯定會偶合在一起的。

現在我們面對的問題是這樣的。在物理引擎裡面,我們經常需要判斷兩個物體是否碰撞。但是物體又不只是三角形組成的多面體,還有可能是標準的球形啊、立方體什麼的。因此這顯然還是一個繼承的結構,而且還有一個虛擬函式用來判斷一個物件跟另一個物件是否碰撞:

class Geometry

{

public:

    virtual ~Geometry();

    virtual bool IsCollided(Geometry* second) = 0;

};

class Sphere : public Geometry

{

public:

    bool IsCollided(Geometry* second)override

    {

        // then ???

    }

};

class Cube : public Geometry

{

public:

    bool IsCollided(Geometry* second)override

    {

        // then ???

    }

};

class Triangles : public Geometry

{

public:

    bool IsCollided(Geometry* second)override

    {

        // then ???

    }

};

大家猛然發現,在這個函式體裡面也不知道second到底是什麼東西。這意味著,我們還要對second做一次single dynamic dispatch,這也就意味著我們需要新增新的虛擬函式。而且這不是一個,而是很多。他們分別是什麼呢?由於我們已經對first(也就是那個this指標)dispatch過一次了,所以我們要把dispatch的結果告訴second,要讓它在dispatch一次。所以當first分別是Sphere、Cube和Triangles的時候,對second的dispatch應該有不同的邏輯。因此很遺憾的,程式碼會變成這樣:

class Sphere;

class Cube;

class Triangles;

class Geometry

{

public:

    virtual ~Geometry();

    virtual bool IsCollided(Geometry* second) = 0;

    virtual bool IsCollided_Sphere(Sphere* first) = 0;

    virtual bool IsCollided_Cube(Cube* first) = 0;

    virtual bool IsCollided_Triangles(Triangles* first) = 0;

};

class Sphere : public Geometry

{

public:

    bool IsCollided(Geometry* second)override

    {

        return second->IsCollided_Sphere(this);

    }

    bool IsCollided_Sphere(Sphere* first)override

    {

        // Sphere * Sphere

    }

    bool IsCollided_Cube(Cube* first)override

    {

        // Cube * Sphere

    }

    bool IsCollided_Triangles(Triangles* first)override

    {

        // Triangles * Sphere

    }

};

class Cube : public Geometry

{

public:

    bool IsCollided(Geometry* second)override

    {

        return second->IsCollided_Cube(this);

    }

    bool IsCollided_Sphere(Sphere* first

相關推薦

如何設計語言——設計擴充套件型別

在思考怎麼寫這一篇文章的時候,我又想到了以前討論正交概念的事情。如果一個系統被設計成正交的,他的功能擴充套件起來也可以很容易的保持質量這是沒錯的,但是對於每一個單獨給他擴充套件功能的個體來說,這個系統一點都不好用。所以我覺得現在的語言被設計成這樣也是有那麼點道理的。就算是設計Java的那誰,他也不是傻逼,那為

如何設計語言——正則表示式與領域特定語言DSL

幾個月前就一直有博友關心DSL的問題,於是我想一想,我在gac.codeplex.com裡面也建立了一些DSL,於是今天就來說一說這個事情。 建立DSL恐怕是很多人第一次設計一門語言的經歷,很少有人一開始上來就設計通用語言的。我自己第一次做這種事情是在高中寫這個傻逼ARPG的時候了。當時做了一個超

如何設計語言——刪減語言的功能

大家看到這個標題肯定會歡呼雀躍了,以為功能少的語言就容易學。其實完全不是這樣的。功能少的語言如果還適用範圍廣,那所有的概念必定是正交的,最後就會變得跟數學一樣。數學的概念很正交吧,正交的東西都特別抽象,一點都不直觀的。不信?出門轉左看Haskell,還有抽象代數。因此刪減語言的功能是需要高超的技巧的,這跟大家

轉載 -- 如何設計語言——exception和error code c,c++ 還沒看

如何設計一門語言(六)——exception和error code http://www.cppblog.com/vczh/archive/2013/06/10/200920.html   如何設計一門語言(六)——exception和error code 我一直以來對於e

如何設計語言——閉包、lambda和interface

人們都很喜歡討論閉包這個概念。其實這個概念對於寫程式碼來講一點用都沒有,寫程式碼只需要掌握好lambda表示式和class+interface的語義就行了。基本上只有在寫編譯器和虛擬機器的時候才需要管什麼是閉包。不過因為系列文章主題的緣故,在這裡我就跟大家講一下閉包是什麼東西。在理解閉包之前,我們

如何設計語言——exception和error code

我一直以來對於exception的態度都是很明確的。首先exception是好的,否則就不會有絕大多數的語言都支援他了。其次,error code也沒什麼問題,只是需要一個前提——你的語言得跟Haskell一樣有monad和comonad。你看Haskell就沒有exception,大家也寫的很開

黑盒測試用例設計-用例維護

叠代 測試的 部分 開發 用例設計 來源 nbsp 延伸 不同的 六、用例維護—經驗用例 當進入執行測試階段時, 我們總是能發現一些缺陷的出現是出乎我們意料的, 或者說是已有的測試需求和測試用例未能覆蓋的。那麽,對於這部分缺陷,也應當在分析整理後添加到測試需求

零基礎學FPGA腳印之基於FIFO的串列埠傳送機設計全流程及常見錯誤詳解

     今天要寫的是一段基於FIFO的串列埠傳送機設計,之前也寫過串列埠傳送的電路,這次寫的與上次的有幾分類似。這段程式碼也是我看過別人寫過的之後,消化一下再根據自己的理解寫出來的,下面是我寫這段程式碼的全部流程和思路,希望對剛開始接觸的朋友來說有一點點的幫助,也希望有

Hibernate中的多表操作1:單向多對

art 保存 int gen round t對象 情況 映射文件 拋出異常 由“多”方可知“一”方的信息,比如多個員工使用同一棟公寓,員工可以知道公寓的信息,而公寓無法知道員工的信息。 案例一: pojo類 public class Department {

自然語言交流系統 phxnet團隊 創新實訓 個人博客

ren texture left 紋理貼圖 技術分享 sse material asset 結果 在本項目中關於天空盒子的使用的配置方法: 給場景添加天空盒 第二種方式 在菜單欄中選擇:Edit->Render Setting,在保證不在選擇場景中其它文

《Linux內核設計與實現》讀書筆記- 內存管理

enable vmalloc 緩沖 turn lean png border 編譯 不一致 內核的內存使用不像用戶空間那樣隨意,內核的內存出現錯誤時也只有靠自己來解決(用戶空間的內存錯誤可以拋給內核來解決)。 所有內核的內存管理必須要簡潔而且高效。 主要內容: 內

Android項目實戰:解決OOM的種偷懶又有效的辦法

建議 是什麽 cat 解決 blog www. android項目 roi acm 原文:Android項目實戰(十二):解決OOM的一種偷懶又有效的辦法在程序的manifest文件的application節點加入android:largeHeap=“true&

設計模式—— 享元模式

方便 表示 復雜 優缺點 強制 port n) 使用場景 nfa 模式簡介 運用共享技術有效地支持大量細粒度地對象。 通常情況下,面向對象技術可以增強系統地靈活性及可擴展性,在系統開發過程中,我們會不斷地增加類和對象。當對象數量過多時,將會帶來系統開銷過高、性能下降等

C++語言學習——C++語言常見函數調用約定

調用函數 操作 開發 混合 類成員 修飾 fast 順序 處理 C++語言學習(十二)——C++語言常見函數調用約定 一、C++語言函數調用約定簡介 C /C++開發中,程序編譯沒有問題,但鏈接的時候報告函數不存在,或程序編譯和鏈接都沒有錯誤,但只要調用庫中的函數就會出現堆

Go語言開發、Go語言常用標準庫

after 更新 use har 相等 文件的 環境變量 its 內核 Go語言開發(十二)、Go語言常用標準庫二 一、os 1、os簡介 os 包提供了不依賴平臺的操作系統函數接口,設計像Unix風格,但錯誤處理是go風格,當os包使用時,如果失敗後返回錯誤類型而不是錯誤

工作那些事談談碼農與農民工區別和發展之路 工作那些事如果哪天,沒有了電腦 工作那些事十三再次失業

工作那些事系列連結快速通道,不斷更新中: 工作那些事(一)今年工作不好找 工作那些事(二)應聘時填寫個人資訊ABCD 工作那些事(三)什麼樣的公司能吸引你,什麼樣的公司適合你? 工作那些事(四)大公司VS小公司 工作那些事(五)談談專案資料整理和積累 工作那些事(六)談談

程式設計菜鳥到大佬之路:C語言程式

第十二天學習精要 遞迴初步 遞迴 一個函式,自己呼叫自己,就是遞迴。 # include <iostream> using namespace std; int factorial(int n) // 函式返回n的階乘 { if (n ==

Java併發:CAS Unsafe Atomic 說說Java的Unsafe類 說說Java的Unsafe類 Java中Unsafe類詳解 Unsafe與CAS

一、Unsafe Java無法直接訪問底層作業系統,而是通過本地(native)方法來訪問。不過儘管如此,JVM還是開了一個後門,JDK中有一個類Unsafe,它提供了硬體級別的原子操作。 這個類儘管裡面的方法都是public的,但是並沒有辦法使用它們,JDK API文件也沒有提供任何關於這個類的方法的解

基於SSH框架的電子新聞系統的設計與實現——論文隨筆

一、基本資訊 標題:基於SSH框架的電子新聞系統的設計與實現 時間:2015-05 出版源:電子科技大學 領域分類:系統架構和設計 二、研究背景 問題定義:新聞資訊採編和釋出的及時性和準確性較差,難以保證新聞的時效性,進而導致新聞資訊難以有效共享、缺乏高效統一控制。此外,隨著高校規模的擴大、高校合併,由

Java語言學習:多執行緒

    Java中給多執行緒程式設計提供了內建的支援,多執行緒是多工的一種特別形式,它使用了更小的資源開銷。這裡需要知道兩個術語及其關係:程序和執行緒。     程序:程序是系統進行資源分配和排程的一個獨立單位。一個程序包括由作業系統分配的記憶體空間,包含