• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            隨筆-341  評論-2670  文章-0  trackbacks-0

            我一直以來對于exception的態(tài)度都是很明確的。首先exception是好的,否則就不會有絕大多數(shù)的語言都支持他了。其次,error code也沒什么問題,只是需要一個前提——你的語言得跟Haskell一樣有monad和comonad。你看Haskell就沒有exception,大家也寫的很開心。為什么呢?因?yàn)橹灰逊祷貛rror code結(jié)果的函數(shù)給做成一個monad/comonad,那么就可以用CPS變換把它變成exception了。所以說CPS作為跟goto同樣基本的控制流語句真是當(dāng)之無愧呀,只是CPS是type rich的,goto是type poor的。

            其實(shí)很多人對于exception的恐懼心理在于你不知道一個函數(shù)會拋什么exception出來,然后程序一crash你就傻逼了。對于server來講情況還好,出了問題只要?dú)⒌艨焖僦貑⒕托辛?,如今沒個replication和fault tolerance還有臉說你在寫后端(所以不知道那些做web的人究竟在反對什么)?這主要的問題還是在于client。只要client上面的東西還沒保存,那你一crash數(shù)據(jù)就完蛋了是不是——當(dāng)然這只是你的想象啦,其實(shí)根本不是這樣子的。

            我們的程序拋了一個access violation出來,和拋了其它exception出來,究竟有什么區(qū)別呢?access violation是一個很奇妙的東西,一旦拋了出來就告訴你你的程序沒救了,繼續(xù)執(zhí)行下去說不定還會有破壞作用。特別是對于C/C++/Delphi這類語言來說,你不小心把錯誤的東西寫進(jìn)了什么亂七八糟的指針里面去,那會兒什么事情都沒發(fā)生,結(jié)果程序跑著跑著就錯了。因?yàn)槟隳莻€算錯了得到的野指針,說不定是隔壁的不知道什么object的成員變量,說不定是heap里面的數(shù)據(jù)結(jié)構(gòu),或者說別的什么東西,就這么給你寫了。如果你寫了別的object的成員變量那封裝肯定就不管用了,這個類的不變量就給你破壞了。既然你的成員函數(shù)都是基于不變量來寫的,那這個時候出錯時必須的。如果你寫到了heap的數(shù)據(jù)結(jié)構(gòu)那就更加呵呵呵了,說不定下次一new就崩了,而且你還不知道為什么。

            出了access violation以外的exception基本是沒什么危害的,最嚴(yán)重的大概也就是網(wǎng)線被拔了,另一塊不是裝OS的硬盤突然壞了什么的這種反正你也沒辦法但是好歹還可以處理的事情。如果這些exception是你自己拋出來的那就更可靠了——那都是計(jì)劃內(nèi)的。只要程序未來不會進(jìn)入access violation的狀態(tài),那證明你現(xiàn)在所能拿到的所有變量,還有指針指向的memory,基本上都還是靠譜的。出了你救不了的錯誤,至少你還可以吧數(shù)據(jù)安全的保存下來,然后讓自己重啟——就跟word一樣。但是你有可能會說,拿出了access violation怎么就不能保存數(shù)據(jù)了呢?因?yàn)檫@個時候內(nèi)存都?xì)Я?,指不定你保存?shù)據(jù)的代碼new點(diǎn)東西然后掛了,這基本上是沒準(zhǔn)的。

            所以無論你喜歡exception還是喜歡error code,你所希望達(dá)到的效果本質(zhì)上就是避免程序未來會進(jìn)入access violation的狀態(tài)。想做到這一點(diǎn),方法也是很簡單粗暴的——只要你在函數(shù)里面把運(yùn)行前該對函數(shù)做的檢查都查一遍就好了。這個無論你用exception還是用error code,寫起來都是一樣的。區(qū)別在于調(diào)用你的函數(shù)的那個人會怎么樣。那么我來舉個例子,譬如說你覺得STL的map實(shí)在是太傻比了,于是你自己寫了一個,然后有了一個這樣子的函數(shù):

            // exception版本
            Symbol* SymbolMap::Lookup(const wstring& name);
            
            // error code版本
            int SymbolMap::Lookup(const wstring& name, Symbol*& result);
            
            // 其實(shí)COM就是你們最喜歡的error code風(fēng)格了,寫起來應(yīng)該很開心才對呀,你們的雙重標(biāo)準(zhǔn)真嚴(yán)重
            HRESULT ISymbolMap::Lookup(BSTR name, ISymbol** result);

            于是拿到了Lookup函數(shù)之后,我們就要開始來完成一個任務(wù)了,譬如說拿兩個key得到兩個symbol然后組合出一個新的symbol。函數(shù)的錯誤處理邏輯是這樣的,如果key失敗了,因?yàn)闃I(yè)務(wù)的原因,我們要告訴函數(shù)外面說key不存在的。調(diào)用了一個ComposeSymbol的函數(shù)丟出什么IndexOutOfRangeException顯然是不合理的。但是合并的那一步,因?yàn)闃I(yè)務(wù)都在同一個領(lǐng)域內(nèi),所以suppose里面的異常外面是可以接受的。如果出現(xiàn)了計(jì)劃外的異常,那我們是處理不了的,只能丟給上面了,外面的代碼對于不認(rèn)識的異常只需要報(bào)告任務(wù)失敗了就可以了。于是我們的函數(shù)就會這么寫:

            Symbol* ComposeSymbol(const wstring& a, const wstring& b, SymbolMap* map)
            {
                Symbol* sa=0;
                Symbol* sb=0;
                try
                {
                    sa=map->Lookup(a);
                    sa=map->Lookup(b);
                }
                catch(const IndexOutOfRangeException& ex)
                {
                    throw SymbolKeyException(ex.GetIndex());
                }
                return CreatePairSymbol(sa, sb);
            }

            看起來還挺不錯?,F(xiàn)在我們可以開始考慮error code的版本了。于是我們需要思考幾個問題。首先第一個就是Lookup失敗的時候要怎么報(bào)告?直接報(bào)告key的內(nèi)容是不可能的,因?yàn)閑rror code是個int。

            題外話,error code當(dāng)然可以是別的什么東西,如果需要返回豐富內(nèi)容的錯誤的話,那怎樣都得是一個指針了,這個時候你們就會面臨下面的問題——這已經(jīng)他媽不滿足誰構(gòu)造誰釋放的原則了呀,而且我這個指針究竟直接返回出去外面理不理呢,如果只要有一個環(huán)節(jié)不理了,那內(nèi)存豈不是泄露了?如果我要求把錯誤返回在參數(shù)里面的話,我每次調(diào)用函數(shù)都要創(chuàng)建出那么個結(jié)構(gòu)來保存異常,不僅有if的復(fù)雜度,還有創(chuàng)建空間的復(fù)雜度,整個代碼都變成了屎。所以還是老老實(shí)實(shí)用int吧……

            那我們要如何把key的信息給編碼在一個int里面呢?因?yàn)閗ey要么是來自于a,要么是來自于b,所以其實(shí)我們就需要兩個code了。那Lookup的其他錯誤怎么辦呢?CreatePairSymbol的錯誤怎么辦呢?萬一Lookup除了ERROR_KEY_NOT_FOUND以外,或者是CreatePairSymbol的錯誤剛好跟a或者b的code重合了怎么辦?對于這個問題,我只能說:

            要不你們team的人先開會討論一下最后記錄在文檔里面?zhèn)洳橐悦夂竺娴娜丝戳松笛哿恕?/font>

            好了,現(xiàn)在假設(shè)說會議取得了圓滿成功,會議雙方加深了互相的理解,促進(jìn)了溝通,最后還寫了一個白皮書出來,有效的落實(shí)了對a和b的code的指導(dǎo),于是我們終于可以寫出下面的代碼了:

            #define SUCCESS 0 // global error code for success
            #define ERROR_COMPOSE_SYMBOL_WRONG_A 1
            #define ERROR_COMPOSE_SYMBOL_WRONG_B 2
            
            int ComposeSymbol(const wstring& a, const wstring& b, SymbolMap* map, Symbol*& result)
            {
                int code=SUCCESS;
                Symbol* sa=0;
                Symbol* sb=0;
                switch(code=map->Lookup(a, sa))
                {
                case SUCCESS:
                    break;
                case ERROR_SYMBOL_MAP_KEY_NOT_FOUND:
                    return ERROR_COMPOSE_SYMBOL_WRONG_A;
                default:
                    return code;
                }
                switch(code=map->Lookup(b, sb))
                {
                case SUCCESS:
                    break;
                case ERROR_SYMBOL_MAP_KEY_NOT_FOUND:
                    return ERROR_COMPOSE_SYMBOL_WRONG_B;
                default:
                    return code;
                }
                return CreatePairSymbol(sa, sb, result);
            }

            啊,好像太長,干脆我還是不負(fù)責(zé)任一點(diǎn)吧,反正代碼寫的好也漲不了工資,干脆不認(rèn)識的錯誤都返回ERROR_COMPOSE_SYMBOL_UNKNOWN_ERROR好了,于是就可以把代碼變成下面這樣……都到這份上了不要叫自己程序員了,叫程序狗吧……

            #define SUCCESS 0 // global error code for success
            #define ERROR_COMPOSE_SYMBOL_WRONG_A 1
            #define ERROR_COMPOSE_SYMBOL_WRONG_B 2
            #define ERROR_COMPOSE_SYMBOL_UNKNOWN_ERROR 3
            
            int ComposeSymbol(const wstring& a, const wstring& b, SymbolMap* map, Symbol*& result)
            {
                Symbol* sa=0;
                Symbol* sb=0;
                if(map->Lookup(a, sa)!=SUCCESS)
                    return ERROR_COMPOSE_SYMBOL_UNKNOWN_ERROR;
                if(map->Lookup(b, sb)!=SUCCESS)
                    return ERROR_COMPOSE_SYMBOL_UNKNOWN_ERROR;
                if(CreatePairSymbol(sa, sb, result)!=SUCCESS)
                    return ERROR_COMPOSE_SYMBOL_UNKNOWN_ERROR;
                return SUCCESS;
            }

            當(dāng)然,如果大家都一樣不負(fù)責(zé)任的話,還是exception完爆error code:

            Symbol* ComposeSymbol(const wstring& a, const wstring& b, SymbolMap* map)
            {
                return CreatePairSymbol(map->Lookup(a), map->Lookup(b));
            }

            大部分人人只會用在當(dāng)前條件下最容易寫的方法來設(shè)計(jì)軟件,而不是先設(shè)計(jì)出軟件然后再看看怎樣寫比較容易,這就是為什么我說,只要你一個月給程序員還給不到一狗半,還是老老實(shí)實(shí)在政策上落實(shí)exception吧。至少exception寫起來還不會讓人那么心煩,可以把程序?qū)懙脠?jiān)固一點(diǎn)。

            好了,單線程下面至少你還可以爭吵說究竟exception好還是error code好,但是到了異步程序里面就完全不一樣了。現(xiàn)在的異步程序都很多,譬如說有良心的手機(jī)app啦,譬如說javascript啦,metro程序等等。一個try根本沒辦法跨線程使用所以一個這樣子的函數(shù)(下面開始用C#,C++11的future/promise我用的還不熟):

            class Normal
            {
                public string Do(string args);
            }

            最后就會變成這樣:

            class Async
            {
                // before .NET 4.0
                IAsyncResult BeginDo(string args, Action<IAsyncResult> continuation);
                string EndDo(IAsyncResult ar);
            
                // after .NET 4.0
                Task<string> DoAsync(string args);
            }

            當(dāng)你使用BeginDo的時候,你可以在continuation里面調(diào)用EndDo,然后得到一個string,或者得到一個exception。但是因?yàn)镋ndDo的exception不是在BeginDo里面throw出來的,所以無論你EndDo返回string也好,返回Tuple<string, Exception>也好,對于BeginDo和EndDo的實(shí)現(xiàn)來說其實(shí)都一樣,沒有上文所說的exception和error code的區(qū)別。

            不過.NET從BeginDo/EndDo到DoAsync經(jīng)歷了一個巨大的進(jìn)步。雖然形式上都一樣,但是由于C#并不像Haskell那樣可以完美的操作函數(shù),C#還是面向?qū)ο笞龅酶茫谑侨绻覀儼蒚ask<T>看成下面的樣子,那其實(shí)兩種寫法是沒有區(qū)別的:

            class Task<T>
            {
                public IAsyncResult BeginRun(Action<IAsyncResult> continuation);
                public T EndRun(IAsyncResult ar);
            }

            不過如果還是用BeginRun/EndRun這種方法來調(diào)用的話,使用起來還是很不方便,而且也很難把更多的Task組合在一起。所以最后.NET給出的Task是下面這個樣子的(Comonad!):

            class Task<T>
            {
                public Task<U> ContinueWith<U>(Func<Task<T>, U> continuation);
            }

            盡管真實(shí)的Task<T>要比上面那個復(fù)雜得多,但是總的來說其實(shí)就是圍繞著基本簡單的函數(shù)建立起來的一大堆helper function。到這里C#終于把CPS變換在異步處理上的應(yīng)用的這一部分給抽象出來了。在看CPS的效果之前,我們先來看一個同步函數(shù):

            void button1_Clicked(object sender, EventArgs e)
            {
                    // 假設(shè)我們有string Http.Download(string url);
                    try
                    {
                            string a = Http.Download(url1);
                            string b = Http.Download(url2);
                            textBox1.Text=a+b;
                    }
                    catch(Exception ex)
                    {
                            textBox1.Text=ex.Message;
                    }
            }

            這段代碼顯然是一個GUI里面的代碼。我們?nèi)绻谝粋€GUI程序里面這么寫,就會把程序?qū)懙酶鶴Q一樣卡了。所以實(shí)際上這么做是不對的。不過為了表達(dá)程序需要做的所有事情,就有了這么一個同步的版本。那么我們嘗試吧這個東西修改成異步的把!

            void button2_Clicked(object sender, EventArgs e)
            {
                // 假設(shè)我們有Task<string> Http.DownloadAsync(string url);
                // 需要MethodInvoker是因?yàn)椋瑢extBox1.Text的修改只能在GUI線程里面做
                Http.DownloadAsync(url1).ContinueWith(ta=>new MethodInvoker(()=>
                {
                    try
                    {
                        // 這個時候ta已經(jīng)運(yùn)行完了,所以對ta.Result的取值不會造成GUI線程等待IO。
                        // 而且如果DownloadAsync內(nèi)部出了錯,異常會在這里拋出來。
                        string a=ta.Result;
                        Http.DownloadAsync(url2).ContinueWith(tb=>new MethodInvoker(()=>
                        {
                            try
                            {
                                string b=tb.Result;
                                textBox1.Text=a+b;
                            }
                            catch(Exception ex)
                            {
                                textBox1.Text=ex.Message;
                            }
                        })));
                    }
                    catch(Exception ex)
                    {
                        textBox1.Text=ex.Message;
                    }
                })));
            }

            我們發(fā)現(xiàn),異步操作發(fā)生的異常,把優(yōu)越的exception拉低到了丑陋的error code的同一個情況上面——我們需要不斷地對每一個操作重復(fù)同樣的錯誤處理過程!而且在這種地方我們連“不負(fù)責(zé)任”的選項(xiàng)都沒有了,如果你不try-catch(或者不檢查error code),那到時候程序就會發(fā)生一些莫名其妙的問題,在GUI那一層你什么事情都不知道,整個程序就變成了傻逼。

            現(xiàn)在可以開始解釋一下什么是CPS變換了。CPS變換就是把所有g(shù)(f(x))都給改寫成f(x, r=>g(r))的過程。通俗一點(diǎn)講,CPS變換就是幫你把那個同步的button1_Click給改寫成異步的button2_Click的這個過程。盡管這么說可能不太嚴(yán)謹(jǐn),因?yàn)閎utton1_Click跟button2_Click所做的事情是不一樣的,一個會讓GUI卡成qq,另一個不會。但是我們討論CPS變換的時候,我們討論的是對代碼結(jié)構(gòu)的變換,而不是別的什么東西。

            現(xiàn)在就是激動人心的一步了。既然CPS可以把返回值變換成lambda表達(dá)式,那反過來我們也可以把所有的以這種形式存在的lambda表達(dá)式都改寫成返回值嘛?,F(xiàn)在我們滾回去看一看button2_Click,會發(fā)現(xiàn)這個程序其實(shí)充滿了下面的pattern:

            // lambda的參數(shù)名字故意起了跟前面的變量一樣的名字(previousTask)因?yàn)槠鋵?shí)他們就是同一個東西
            previousTask.ContinueWith(previousTask=>new MethodInvoker(()=>
            {
                try
                {
                    continuation(previousTask.Result);
                }
                catch(Exception ex)
                {
                    textBox1.Text=ex.Message;
                }
            })));

            我們可以“發(fā)明”一個語法來代表這個過程。C#用的是await關(guān)鍵字,那我們也來用await關(guān)鍵字。假設(shè)說上面的代碼永遠(yuǎn)等價(jià)于下面的這個代碼:

            try
            {
                var result=await previousTask;
                continuation(result);
            }
            catch(Exception ex)
            {
                textBox1.Text=ex.Message;
            }

            兩段代碼的關(guān)系就跟i++;和i=i+1;一樣是可以互相替換的,只是不同的寫法而已。那我們就可以用相同的方法來把button2_Click給替換成下面的button3_Click了:

            void button3_Click(object sender, EventArgs e)
            {
                try
                {
                    var a=await Http.DownloadAsync(url1);
                    try
                    {
                        var b=await Http.DownloadAsync(url2);
                        textBox1.Text=a+b;
                    }
                    catch(Exception ex)
                    {
                        textBox1.Text=ex.Message;
                    }
                }
                catch(Exception ex)
                {
                    textBox1.Text=ex.Message;
                }
            }

            聰明的讀者立刻就想到了,兩個try其實(shí)是重復(fù)的,那為什么不把他們合并成一個呢!當(dāng)然我想告訴大家的是,異常是在不同的線程里面拋出來的,只是我們用CPS變換把代碼“改寫”成這種形式而已。理論上兩個try是不能合并的。但是!我們的C#編譯器君是很聰明的。正所謂語言的抽象高級了一點(diǎn),那么編譯器對你的代碼也就理解得更多了一點(diǎn)。如果編譯器發(fā)現(xiàn)你在try里面寫了兩個await,馬上就明白了過來他需要幫你復(fù)制catch的部分——或者說他可以幫你自動的復(fù)制catch的部分,那情況就完全不同了,最后就可以寫成:

            // C#要求函數(shù)前面要加一個async來允許你在函數(shù)內(nèi)使用await
            // 當(dāng)然同時你的函數(shù)也就返回Task而不是void了
            // 不過沒關(guān)系,C#的event也可以接受一個標(biāo)記了async的函數(shù),盡管返回值不一樣
            // 設(shè)計(jì)語言這種事情就是牽一發(fā)而動全身呀,加個await連event都要改
            async void button4_Click(object sender, EventArgs e)
            {
                try
                {
                    string a=await Http.DownloadAsync(url1);
                    string b=await Http.DownloadAsync(url2);
                    textBox1.Text=a+b;
                }
                catch(Exception ex)
                {
                    textBox1.Text=ex.Message;
                }
            }

            把兩個await換成回調(diào)已經(jīng)讓我們寫的夠辛苦了,那么如果我們把a(bǔ)wait寫在了循環(huán)里面,事情就不那么簡單了。CPS需要把循環(huán)翻譯成遞歸,那你就得把lambda表達(dá)時拿出來寫成一個普通的函數(shù)——這樣他就可以有名字了——然后才能遞歸(寫出一個用于CPS的Y-combinator是一件很困難的事情,盡管并沒有比Y-combinator本身困難多少)。這個例子就復(fù)雜到爆炸了,我在這里就不演示了。

            總而言之,C#因?yàn)橛辛薈PS變換(await),就可以把button4_Click幫你寫成button3_Click然后再幫你寫成button2_Click,最后把整個函數(shù)變成異步和回調(diào)的形式(真正的做法要更聰明一點(diǎn),大家可以反編譯去看)在異步回調(diào)的寫法里面,exception和error code其實(shí)是一樣的。但是CPS+exception和CPS+error code就跟單線程下面的exception和error code一樣,有著重大的區(qū)別。這就是為什么文章一開始會說,我只會在帶CPS變換的語言(Haskell/F#/etc)里面使用error code。

            在這類語言里面利用相同的技巧,就可以不是異步的東西也用CPS包裝起來,譬如說monadic parser combinator。至于你要選擇monad還是comonad,基本上就是取決于你要自動提供錯誤處理還是要手動提供錯誤處理。像上面的Task.ContinueWith,是要求你手動提供錯誤處理的(因?yàn)槟鉩atch了之后可以干別的事情,Task無法自動替你選擇最好的措施),所以他就把Task.ContinueWith寫成了comonad的那個樣子。

            寫到這里,不禁要同情寫前端的那幫javascript和自以為可以寫后端的node.js愛好者們,你們因?yàn)樾⌒〉膃val的問題,不用老趙的windjs(windjs給javascript加上了await但是它不是一個altjs所以得顯式調(diào)用eval),是一個多大的損失……

            posted on 2013-06-09 23:01 陳梓瀚(vczh) 閱讀(13974) 評論(8)  編輯 收藏 引用 所屬分類: 啟示

            評論:
            # re: 如何設(shè)計(jì)一門語言(六)&mdash;&mdash;exception和error code 2013-06-09 23:40 | sorra
            C#的這個特性挺棒的!  回復(fù)  更多評論
              
            # re: 如何設(shè)計(jì)一門語言(六)&mdash;&mdash;exception和error code 2013-06-09 23:55 | DiryBoy
            # re: 如何設(shè)計(jì)一門語言(六)&mdash;&mdash;exception和error code 2013-06-10 12:15 | jagd
            Haskell 不僅有 Control.Exception 而且用起來特麻煩, 有時還非用不可 (例如判斷 EOF)  回復(fù)  更多評論
              
            # re: 如何設(shè)計(jì)一門語言(六)&mdash;&mdash;exception和error code 2013-06-10 20:38 | 陳梓瀚(vczh)
            @jagd
            haskell寫帶副作用的程序是很痛苦的  回復(fù)  更多評論
              
            # re: 如何設(shè)計(jì)一門語言(六)&mdash;&mdash;exception和error code 2013-06-12 02:36 | BYVois
            C#裏面的await獲取異步異常跟我的Continuation.js中對同步異步異常統(tǒng)一用try..catch處理完全一樣。  回復(fù)  更多評論
              
            # re: 如何設(shè)計(jì)一門語言(六)&mdash;&mdash;exception和error code 2013-06-12 06:28 | 陳梓瀚(vczh)
            @BYVois
            當(dāng)然了,因?yàn)楹瘮?shù)式只有這種處理方法……不過C#的做法不是直接的CPS手法,他是把整個代碼編譯成了一個狀態(tài)機(jī),減少了很多call來call去的消耗,產(chǎn)生的代碼也就少了很多。  回復(fù)  更多評論
              
            # re: 如何設(shè)計(jì)一門語言(六)&mdash;&mdash;exception和error code[未登錄] 2013-11-10 03:25 | patz
            問個問題,在這段代碼里:

            void button2_Clicked(object sender, EventArgs e)
            {
            Http.DownloadAsync(url1).ContinueWith(ta=>new MethodInvoker(()=>
            {
            try
            {
            // 這個時候ta已經(jīng)運(yùn)行完了,所以對ta.Result的取值不會造成GUI線程等待IO。
            // 而且如果DownloadAsync內(nèi)部出了錯,異常會在這里拋出來。
            string a=ta.Result;

            這里的ContinueWith應(yīng)該是在別的線程里去執(zhí)行,所以即使ta沒有運(yùn)行完,ta.Result也不回造成UI線程hang。(當(dāng)然ta肯定運(yùn)行完了)

            是這樣吧?  回復(fù)  更多評論
              
            # re: 如何設(shè)計(jì)一門語言(六)&mdash;&mdash;exception和error code 2013-11-10 05:49 | 陳梓瀚(vczh)
            @patz
            不管執(zhí)行完沒有,ta.Result都會進(jìn)一個鎖等結(jié)果好了再返回。所以如果你調(diào)用Result的時候已經(jīng)算好了,那也是瞬間獲得鎖然后返回。不過await編譯出來的代碼在訪問Result的時候都保證已經(jīng)執(zhí)行完了。  回復(fù)  更多評論
              
            久久久WWW成人免费毛片| 国产成人精品久久亚洲| 亚洲午夜无码久久久久| 久久精品国产亚洲av高清漫画 | 97久久超碰国产精品旧版 | 久久久WWW成人免费毛片| 亚洲国产成人久久笫一页| 少妇久久久久久久久久| 色综合久久中文色婷婷| 欧美精品乱码99久久蜜桃| 国产成人久久激情91| 超级97碰碰碰碰久久久久最新| 国内精品九九久久久精品| 久久人妻少妇嫩草AV蜜桃| 激情伊人五月天久久综合| 无码8090精品久久一区 | 66精品综合久久久久久久| 国产色综合久久无码有码| 久久99精品久久久久久野外| 日日躁夜夜躁狠狠久久AV| 青青热久久国产久精品| 色综合合久久天天综合绕视看| 久久精品国产久精国产果冻传媒| 国产99久久久久久免费看 | 欧美精品久久久久久久自慰| 久久国产成人亚洲精品影院| 青青青国产成人久久111网站| 午夜精品久久久久久99热| 久久久久久精品无码人妻| 无码人妻久久一区二区三区蜜桃| 国产成人久久精品二区三区| 国产一级持黄大片99久久| 国内精品久久久久影院优| 性高湖久久久久久久久| 亚洲色欲久久久综合网| 久久精品aⅴ无码中文字字幕不卡| 久久久久亚洲精品无码网址| 久久精品中文字幕第23页| 精品国产乱码久久久久久浪潮| 四虎国产精品免费久久久| 国产高潮国产高潮久久久91 |