亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 編程 > C# > 正文

C#中的IEnumerable接口深入研究

2020-01-24 02:39:59
字體:
來源:轉載
供稿:網友

C#和VB.NET中的LINQ提供了一種與SQL查詢類似的“對象查詢”語言,對于熟悉SQL語言的人來說除了可以提供類似關聯、分組查詢的功能外,還能獲取編譯時檢查和Intellisense的支持,使用Entity Framework更是能夠自動為對象實體的查詢生成SQL語句,所以很受大中型信息系統設計者的青睞。

IEnumerable這個接口可以說是為了這個特性“量身定制”,再加上微軟提供的擴展(Extension)方法和Lambda表達式,給開發者帶來了無窮的便利。本人在最近的開發工作中使用了大量的這種特性,同時在調試過程中還遇到了一個小問題,那么正好趁此機會好好研究一下相關原理和實現。

先從一個現實的例子開始吧。假如我們要做一個商品檢索功能(這只是一個例子,我當然不可能把公司的產品也業務在這里貼出來),其中有一個檢索條件是可以指定廠家的名稱并進行模糊匹配。廠家的包括兩個名稱:注冊名稱和一般性名稱,我們只按一般性名稱進行檢索。當然你可以說直接用SQL查詢就行了,但是我們的系統是以實體對象為核心進行設計的,廠家的數量也不會太多,大概1000條。為了不增加系統的復雜性,只考慮使用現有的數據訪問層接口進行實現(按過濾條件獲取商品,以及獲取所有廠商),這時LINQ的便捷性就體現出來了。

借助IEnumerable接口和其輔助類,我們可以寫出以下代碼:

復制代碼 代碼如下:

public GoodsListResponse GetGoodsList(GoodsListRequest request)
{
    //從數據庫中按商品類別獲取商品列表
    IEnumerable<Goods> goods = GoodsInformation.GetGoodsByCategory(request.CategoryId);

    //用戶指定了商品名檢索字段,進行模糊匹配
    //如果沒有指定,則不對商品名進行過濾
    if (!String.IsNullOrWhiteSpace(request.GoodsName))
    {
        request.GoodsName = request.GoodsName.Trim().ToUpper();
       
        //按商品名對 goods 中的對象進行過濾
        //生成一個新的 IEnumerable<Goods> 類型的迭代器
        goods = goods.Where(g => g.GoodsName.ToUpper().Contains(request.GoodsName));
    }

    //如果用戶指定的廠商的檢索字段,進行模糊匹配
    if (!String.IsNullOrWhiteSpace(request.ManufactureName))
    {
        request.ManufactureName = request.ManufactureName.Trim().ToUpper();

        //只提供了獲取所有廠商的列表方法
        //取出所有廠商,篩選包含關鍵字的廠商
        IEnumerable<Manufacture> manufactures = ManufactureInformation.GetAll();
        manufactures = manufactures.Where(m => m.Name.GeneralName.ToUpper()
                            .Contains(request.ManufactureName));

        //取出任何符合所匹配廠商的商品
        goods = goods.Where(g => manufactures.Any(m => m.Id == g.ManufactureId));
    }

    GoodsListResponse response = new GoodsListResponse();

    //將 goods 放到一個 List<Goods> 對象中,并返回給客戶端
    response.GoodsList = goods.ToList();

    return response;
}

假如不使用IEnumerable這個接口,所實現的代碼遠比上面復雜且難看。我們需要寫大量的foreach語句,并手工生成很多中間的 List 來不斷地篩選對象(你可以嘗試把第二個if塊改寫成不用IEnumerable接口的形式)。

看上去一切都很和諧,但是上面的代碼有一個隱含的bug,這個bug也是今天上午困擾了我許久的一個問題。

運行程序,當我不輸入廠商檢索條件的時候,程序運行是正確的。但當我輸入一個廠商的名字時,系統拋出了一個空引用的異常。咦?為什么會有空引用呢?我輸入的廠商是數據庫中不存在的廠商,因此我覺得問題可以出在goods = goods.Where(g => manufactures.Any(m => m.Id == g.ManufactureId)) 這句話上。既然manufactures是空的,那么是不是意味著我不能調用其 Any 方法呢(lambda表達式中的部分)。于是我改寫成以下形式:

復制代碼 代碼如下:

if (manufactures != null)
    //取出任何符合所匹配廠商的商品
    goods = goods.Where(g => manufactures.Any(m => m.Id == g.ManufactureId));

還是不行,那么我對manufactures判斷其是否有元素,就調用其無參數的Any方法,這時問題依舊:

聰明的你肯定已經看出問題出在哪了,因為Visual Studio已經提示得很清楚了。但我當時還局限在“列表為空”這個框框中,因此遲遲不能發現原因。出錯是發生在 manufactures.Any() 這句話上,而我已經判斷了它不為空啊,為什么還會拋錯呢?

后來叫了一個同事幫我看,他說的四個字一下子就提醒了我“延遲計算”。哦,對!我怎么把這個特性給忘了。在最初的代碼中(就是沒有對 manufactures 為空進行判斷),出錯是發生在 goods.ToList() 這句話時,而圖上的那個代碼段出錯是發生在調用Any()方法時(圖中的灰色部分),而我單步跟蹤到 Any() 這句話上時,出錯的語句跳到 Where 子句(黃色部分),說明知道訪問 Any 方法時lambda表達式才被調用。

那么很顯然是 Where 語句中這個 predicate 有問題:Manufacture的Name字段可能為空(數據庫中存在這樣的數據,所以導致在 translate 的時候Name字段為空),那么改寫成以下形式就能解決問題,當然我們不用對 manufactures 列表進行為空的判斷:

復制代碼 代碼如下:

manufactures = manufactures.Where(m => m.Name != null &&
                    m.Name.GeneralName.ToUpper().Contains(request.ManufactureName));

在此要感謝那位同事看出了問題所在,否則我不知道還得郁悶多久。

我之前在使用 LINQ 語句的時候知道它的延遲計算特性,但是沒有想到從根本上自 IEnumerable 的擴展方法就有這個特性。那么很顯然,C#的編譯器只是把 LINQ 語句改寫成類似于調用 Where、Select之類的擴展方法,延遲計算這種特性是 IEnumerable 的擴展方法就支持的!我之前一直以為我每調用一次 Where 或者 Select(其實我SelectMany用得更多),就會對結果進行過濾,現在看來并不是這樣。

即使是使用 Where 等擴展方法, 執行這些 predicate 的時間是在 foreach 和 ToList 的時候才發生。

為什么會這樣呢?看樣子這完全不應該呀?Where子句的返回值就是一個IEnumerable的迭代器,按道理應該已經篩選了對象?。繛榱藦氐赘闱宄@個問題,那么方法很明顯――看 .NET 的源代碼。

Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) 是它的方法頭,在看源代碼之前,相信你已經知道微軟大概是怎么實現的了:既然Where接受一個Func類型的委托,并且都是在ToList 或者 foreach 的時候計算的,那么顯而易見實現應該是……

好了,來看下代碼吧。IEnumerable的擴展方法都在 Enumerable 這個靜態類中,Where方法的實現代碼如下:

復制代碼 代碼如下:

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) {
    if (source == null) throw Error.ArgumentNull("source");
    if (predicate == null) throw Error.ArgumentNull("predicate");
    if (source is Iterator<TSource>) return ((Iterator<TSource>)source).Where(predicate);
    if (source is TSource[]) return new WhereArrayIterator<TSource>((TSource[])source, predicate);
    if (source is List<TSource>) return new WhereListIterator<TSource>((List<TSource>)source, predicate);
    return new WhereEnumerableIterator<TSource>(source, predicate);
}

很顯然,M$會用到 source 的類型,根據不同的類型返回不同的 WhereXXXIterator。等等,這就意味著Where方法返回的不是IEnumerable。從這里我們就可以清晰地看到M$其實是包裝了一層,那么顯而易見,應該是只記錄了一個委托。這些WhereXXXIterator都是派生自 Iterator 抽象類,這個類實現了 IEnumerable<TSource> 和 IEnumerator<TSource> 這兩個接口,這樣用戶就能鏈式地去調用。不過, Iterator 類不是public的,所以用戶只知道是一個  IEnumerable 的類型。這樣做的好處是可以向用戶隱藏一些底層實現的細節,顯得類庫用起來很簡單;壞處是可能會導致用戶的使用方式不合理,以及一些較難理解的問題。

我們暫時不看 Iterator 類的一些細節,繼續看 WhereListIterator 的 Where 方法。這個方法在基類是抽象的,因此在這里實現它:

復制代碼 代碼如下:

public override IEnumerable<TSource> Where(Func<TSource, bool> predicate) {
    return new WhereListIterator<TSource>(source, CombinePredicates(this.predicate, predicate));
}

CombinePredicates是Enumerable靜態類提供的擴展方法,不過它不是public的,只有在內部才能訪問:

復制代碼 代碼如下:

static Func<TSource, bool> CombinePredicates<TSource>(Func<TSource, bool> predicate1, Func<TSource, bool> predicate2) {
    return x => predicate1(x) && predicate2(x);
}

自然,WhereListIterator 有幾個字段:

復制代碼 代碼如下:

List<TSource> source;
Func<TSource, bool> predicate;
List<TSource>.Enumerator enumerator;

這樣,相信大家都已經知道了Where的工作原理,簡單地總結一下:


1.當我們創建了一個 List 后,調用其定義在 IEnumerable 接口上的 Where 擴展方法,系統會生成一個 WhereListIterator 的對象。這個對象把 Where 子句的 predicate 委托保存并返回。

2.再次調用 Where 子句時,對象其實已經變成 WhereListIterator類型,此后再次調用 Where 方法時,會調用 WhereListIterator.Where 方法,這個方法把兩個 predicate 合并,之后返回一個新的 WhereListIterator。

3.之后的每一次 Where 調用都是執行第2步操作。

可以看出,在調用 Where 方法時,系統只是記錄了 predicate 委托,并沒有回調這些委托,所以此時自然而然就不會產生新的列表。

當遇到foreach語句時,會需要生成一個 IEnumerator 類型的對象以便枚舉,此時就開始調用 Iterator 的 GetEnumerator 方法。這個方法只有在基類中定義:

復制代碼 代碼如下:

public IEnumerator<TSource> GetEnumerator() {
    if (threadId == Thread.CurrentThread.ManagedThreadId && state == 0) {
        state = 1;
        return this;
    }
    Iterator<TSource> duplicate = Clone();
    duplicate.state = 1;
    return duplicate;
}

在獲取迭代器的時候要考慮并發的問題,如果多個線程都在枚舉元素,同時使用一個迭代器肯定會發生混亂。M$的實現方法很聰明,對于同一個線程只使用一個迭代器,當發現是另一個線程調用的時候直接克隆一個。

MoveNext方法在子類中定義,WhereListIterator的實現如下:

復制代碼 代碼如下:

public override bool MoveNext() {
    switch (state) {
        case 1:
            enumerator = source.GetEnumerator();
            state = 2;
            goto case 2;
        case 2:
            while (enumerator.MoveNext()) {
                TSource item = enumerator.Current;
                if (predicate(item)) {
                    current = item;
                    return true;
                }
            }
            Dispose();
            break;
    }
    return false;
}


switch語句寫得不容易看懂。在獲取迭代器后,逐個進行 predicate 回調,返回滿足條件的第一個元素。當遍歷結束后,如果迭代器實現了 IDispose 接口,就調用其 Dispose 方法釋放非托管資源。之后設置基類的 state 屬性為-1,這樣今后就訪問不到這個迭代器了,需要重新創建一個。

至此,終于看到只有在迭代時才進行計算的緣由了。其他的一些Iterator大體上都是類似的,只是MoveNext的實現方式不一樣罷了。至于M$為什么要單獨為 List 和 Array 寫一個單獨的類,對于數組來說可以直接根據下標訪問下一個元素,這樣就可以避免訪問迭代器的 MoveNext 方法,可以提高一點效率。但對于列表來說,其實現方式和普通的類相同,估計是首先想使用不同的實現后來發現不好吧。

其他的擴展方法,比如Select、Repeat、Reverse、OrderBy之類的好像也能鏈式調用,并且可以不限順序任意調用多次。這又是怎么實現的呢?

我們先來看Select方法。類似Where方法,Select也定義了對應的三個Iterator:WhereSelectListIterator、WhereSelectArrayIterator和WhereSelectEnumerableIterator。每一種都定義了Select和Where方法:

復制代碼 代碼如下:

public override IEnumerable<TResult2> Select<TResult2>(Func<TResult, TResult2> selector) {
    return new WhereSelectListIterator<TSource, TResult2>(source, predicate, CombineSelectors(this.selector, selector));
}

public override IEnumerable<TResult> Where(Func<TResult, bool> predicate) {
    return new WhereEnumerableIterator<TResult>(this, predicate);
}

CombineSelectors的代碼如下:

復制代碼 代碼如下:

static Func<TSource, TResult> CombineSelectors<TSource, TMiddle, TResult>(Func<TSource, TMiddle> selector1, Func<TMiddle, TResult> selector2) {
    return x => selector2(selector1(x));
}


這樣子就把Select和Where連起來了。本質上,運行時的類型在WhereXXXIterator和WhereSelectXXXIterator之間進行變換,每次都產生一個新的類型。

你可能會覺得對于每一種方法,M$都定義了一個專門的類,比如OrderByIterator等。但這樣做會引起類的爆炸,同時每一種Iterator為了兼容其他的類這樣要重復寫的東西簡直無法想象。微軟把這些函數分成了兩類,第一類是直接調用迭代器,列舉如下:

1.Reverse:生成一個Buffer對象,倒序輸入后返回 IEnumerable 類型的迭代器。
2.Cast:以object類型取迭代器中的元素并轉型yield return。
3.Union、Ditinct:生成一個Set類型的對象,這個對象會訪問迭代器。
4.Concat、Zip、Take、TakeWhile、Skip、SkipWhile:yield return。


很顯然,調用這些方法會導致訪問迭代器,這樣 predicate 和 selector 就會開始進行回調(如果是WhereXXXIterator或WhereSelectXXXIterator類型的話)。當然,訪問聚集函數或者First之類的方法顯而易見會導致列表進行迭代,這里不多說明了。

第二種就是微軟進行特殊處理的 Join、GroupBy、OrderBy、ThenBy。這幾個方法是 LINQ 中的核心,偷懶怎么行?我已經寫累了,相信各位看官也累了。但是求知心怎么會允許我們休息呢?繼續往下看吧。

先從最熟悉的排序開始。OrderBy方法最簡單的重載如下(順帶一提,方法簽名看似非常復雜,其實使用起來很簡單,因為Visual Studio會自動幫你匹配泛型參數,比如 goods = goods.OrderBy(g => g.GoodsName);):

復制代碼 代碼如下:

public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector);

哇塞,返回值終于不是IEnumerable了,這個IOrderedEnumerable很明顯也是IEnumerable繼承過來的。在實現上,OrderedEnumerable<TSource>是一個實現了該方法的抽象類,OrderedEnumerable<TSource, TKey>繼承自此類,這兩個類都不對外公開。但微軟又公開了接口,這不是很奇怪么?難道是可以讓用戶自行擴展?這點暫時不深究了。

OrderBy擴展方法會返回一個OrderedEnumerable類型的對象,這個類對外公開了 GetEnumerator 方法:

復制代碼 代碼如下:

public IEnumerator<TElement> GetEnumerator() {
    Buffer<TElement> buffer = new Buffer<TElement>(source);
    if (buffer.count > 0) {
        EnumerableSorter<TElement> sorter = GetEnumerableSorter(null);
        int[] map = sorter.Sort(buffer.items, buffer.count);
        sorter = null;
        for (int i = 0; i < buffer.count; i++) yield return buffer.items[map[i]];
    }
}


OK,重點來了:OrderBy也是進行延時操作!也就是說直到調用 GetEnumerator 之前,還是不會回調前面的 predicate 和 selector。這里的排序算法只是一個簡單的快速排序算法,由于不是重點,代碼省略。

到這里估計有些人已經暈了,所以需要再次進行總結。用一個例子來說明,假如我寫了如下這樣的代碼,應該是怎么工作的呢(代碼僅僅是為了說明,沒有實際的意義)?

復制代碼 代碼如下:

goods = goods.OrderBy(g => g.GoodsName);
goods.Where(g => g.GoodsName.Length < 10);

執行完第一句代碼后,類型變成了 OrderedEnumerable ,那么又來一個 Where,情況會怎么樣呢?

由于 OrderedEnumerable 沒有定義 Where 方法,那么又會調用 IEnumerable 的 Where 方法。此時會發生什么呢?由于類型不是 WhereXXXIterator,那么…… 對!那么會生成一個 WhereEnumerableIterator,此時 List 這個信息就已經丟失了。

有個疑問,我接下來再次調用 Where,此時這個 Where 語句并不知道之前的一些 predicate,在接下來的迭代過程中,怎么進行回調呢?

不要忘了,每一個類似這種類型(Enumerable、Iterator),都有一個 source 字段,這個字段就是鏈式調用的關鍵。OrderedEnumerable 類型對象在初始的過程中記錄了 WhereListIterator 這個類型對象的引用并存入 source 字段中,在接下來的 Where 調用里,新生成的 WhereEnumerableIterator 類型對象中,又將 OrdredEnumerable 類型的對象存入 source 中。之后在枚舉的過程中,會按照如下步驟開始執行:

1.枚舉時類型是 WhereEnumerableIterator,進行枚舉時,首先要得到這個對象的 Enumerator。此時系統調用 source 字段的 GetEnumerator。正是那個不太好理解的 switch 語句,曾經一度被我們忽略的 source.GetEnumerator() 在此起了重要的作用。

2.source 字段存儲的是 OrderedEnumerator 類型的對象,我們參考這個對象的 GetEnumerator 方法(就是上面那個帶 Buffer 的),發現它會調用 Buffer 的構造方法將數據填入緩沖區。Buffer 的構造方法代碼我沒有列出,但是其肯定是調用其 source 的枚舉器(事實上如果是集合會調用其 CopyTo)。

3.這時 source 字段存儲的是 WhereListIterator 類型對象,這個類的行為在最開始我們分析過:逐個回調 predicate 和 selector 并 yield return。
4.最后,前面的迭代器生成了,在 MoveNext 的過程中,首先回調 WhereEumerableIterator 的委托,再繼續取 OrderedEnumerable 的元素,直至完成。

看,一切都是如此地“順理成章”。都是歸功于 source 字段。至此,我們已經幾乎了解了 IEnumerable 的全部玄機。

對了,還有 GroupBy 和 Join 沒有進行說明。在此簡單提一下。

這兩個方法的基礎是一個稱之為 LookUp 的類。LookUp表示一個鍵到多個值的集合(比較Dictionary),在實現上是一個哈希表對應到可以擴容的數組。GroupBy 和 Join 借助 LookUp 實現對元素的分組與關聯操作。GroupBy 語句使用了 GroupEnumerator,其原理和上面所述的 OrderedEnumerator 類似,在此不再贅述。如果對 GroupBy 和 Join 的具體實現感興趣,可以自行參看源代碼。

好了,這次關于 IEnumerable 的研究總算告一段落了,我也總算是弄清了其工作原理,解答了心中的疑慮。另外可以看到,在研究的過程中要有耐心,這樣事情才會越來越明朗的。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
中文字幕亚洲欧美日韩在线不卡| 国产精品一区二区三区在线播放| 国产精品99久久久久久人| 国产精品96久久久久久又黄又硬| 激情亚洲一区二区三区四区| 麻豆国产va免费精品高清在线| 亚洲精品美女在线观看播放| 国产丝袜精品第一页| 欧美丰满少妇xxxx| 国产成人精品999| 国产精品久久综合av爱欲tv| 欧美精品一区二区三区国产精品| 国产成人jvid在线播放| 在线观看亚洲区| 亚洲精品一区二区三区婷婷月| 亚洲美女精品久久| 欧美一级成年大片在线观看| 日本韩国欧美精品大片卡二| 国产欧美va欧美va香蕉在| 亚洲综合小说区| 91av免费观看91av精品在线| 青青草原成人在线视频| 国产精品成人v| 国产欧美亚洲视频| 日韩美女在线观看| 日本91av在线播放| 亚洲永久在线观看| 亚洲欧美资源在线| 一区二区欧美在线| 成人午夜高潮视频| 成人av.网址在线网站| 国产亚洲精品美女久久久| 色老头一区二区三区| 亚洲精品日韩丝袜精品| 国产欧美精品一区二区三区介绍| 97色伦亚洲国产| 91热精品视频| 亚洲国产天堂久久综合| 91久久精品国产91久久性色| 热久久免费视频精品| 国产美女91呻吟求| 一本久久综合亚洲鲁鲁| 久久五月天综合| 成人春色激情网| 亚洲激情在线观看视频免费| 欧美大学生性色视频| 国产精品九九久久久久久久| 亚洲大胆人体av| 国内成人精品视频| 欧美激情久久久久久| 97视频在线免费观看| 国产精品久久97| 亚洲最新中文字幕| 欧美日韩中文字幕在线视频| 欧美日韩一区二区在线播放| 国产视频在线观看一区二区| 久久免费视频观看| 久久理论片午夜琪琪电影网| 亚洲91精品在线观看| 亚洲一区二区三区在线免费观看| 精品中文字幕在线观看| 成人网中文字幕| 欧美疯狂性受xxxxx另类| 久久久久久亚洲精品不卡| 亚洲免费一在线| 人九九综合九九宗合| 日韩中文理论片| 黑人精品xxx一区一二区| 久久久久这里只有精品| 国产成人精品一区二区| 欧美一级黄色网| 国产97在线亚洲| 性欧美长视频免费观看不卡| 国外成人性视频| 啪一啪鲁一鲁2019在线视频| 国产亚洲精品美女久久久久| 日本久久久久久久| 欧美第一淫aaasss性| 亚洲一二在线观看| 国产精品99久久久久久白浆小说| 91精品国产九九九久久久亚洲| 久久久国产精品免费| 91性高湖久久久久久久久_久久99| 亚洲精品日韩av| 一本一本久久a久久精品牛牛影视| 欧美国产精品人人做人人爱| 欧美激情久久久| 亚洲欧美激情在线视频| 日韩av网站在线| 国产精品一二三在线| 亚洲欧美日韩在线一区| 久久精品99久久久久久久久| 国产91精品高潮白浆喷水| 欧美日韩国产丝袜美女| 亚洲免费成人av电影| 怡红院精品视频| 麻豆国产精品va在线观看不卡| 欧美性xxxxxxxxx| 欧美午夜视频一区二区| 欧美在线观看www| 日韩欧美国产成人| 国产精品美腿一区在线看| 5566成人精品视频免费| 国产精品久久久久久久久久久久久久| 国产成人免费91av在线| 国产在线拍偷自揄拍精品| 欧美亚洲国产视频小说| 亚洲日韩欧美视频一区| 欧美极品第一页| 国产在线久久久| 久久精品精品电影网| 欧美又大又硬又粗bbbbb| 国产一区二区色| 日韩精品在线私人| 亚洲第一av在线| 国产精品一区二区三区久久| 亚洲tv在线观看| 国产精品午夜视频| 色黄久久久久久| 日韩极品精品视频免费观看| 日本电影亚洲天堂| 免费av一区二区| 国产精品视频自拍| 久久久久一本一区二区青青蜜月| 7m第一福利500精品视频| 色综合久久精品亚洲国产| 日本午夜在线亚洲.国产| 九九九久久久久久| 国产精品成人观看视频国产奇米| 亚洲日本成人女熟在线观看| 欧美日韩一区二区免费在线观看| 国产成+人+综合+亚洲欧洲| 亚洲人成在线观| 中文字幕日韩专区| 日韩av一卡二卡| 国产精品com| 久久亚洲精品网站| 欧美丝袜第一区| 欧美激情中文字幕乱码免费| 亚洲第一国产精品| 亚洲国产另类久久精品| 欧美激情久久久久| 91亚洲永久免费精品| 久久免费视频在线观看| 欧美日韩美女在线观看| 亚洲一区二区三区xxx视频| 久久精品视频在线观看| 国产一区二区三区18| 亚洲欧美日本另类| 久久精品久久久久| 欧美激情视频一区二区三区不卡| 日韩欧亚中文在线| 全球成人中文在线| 欧美激情一二区| 国产91精品黑色丝袜高跟鞋| 92版电视剧仙鹤神针在线观看| 久久久999国产精品| 欧美激情综合亚洲一二区| y97精品国产97久久久久久| 国语对白做受69| 九九视频直播综合网| 欧美激情中文网| 一区二区三区高清国产| 欧美精品久久久久|