[.net 物件導向程式設計基礎] (21) 委託

yubinfeng發表於2015-06-16

[.net 物件導向程式設計基礎] (20)  委託

   上節在講到LINQ的匿名方法中說到了委託,不過比較簡單,沒了解清楚沒關係,這節中會詳細說明委託。

1. 什麼是委託?

學習委託,我想說,學會了就感覺簡單的不能再簡單了,沒學過或者不願瞭解的人,看著就不知所措了,其實很簡單。

委託在.net物件導向程式設計和學習設計模式中非常重要,是學習.net物件導向程式設計必須要學會並掌握的。

委託從字面上理解,就是把做一些事情交給別人來幫忙完成。在C#中也可以這樣理解,委託就是動態呼叫方法。這樣說明,就很好理解了。

平時我們會遇到這樣的例子需要處理,比如有一個動物園(Zoo)(我還是以前面的動物來說吧)裡面有狗(Dog)、雞(Chicken)、羊(Sheep)……,也許還會再進來一些新品種。參觀動物員的人想聽動物叫聲,那麼可以讓管理員協助(動物只聽懂管理員的),這樣就是一個委託的例子。

在實現委託之前,我們先看一下委託的定義:

委託是一個類,它定義了方法的型別,使得可以將方法當作另一個方法的引數來進行傳遞,這種將方法動態地賦給引數的做法,可以避免在程式中大量使用If-Else(Switch)語句,同時使得程式具有更好的可擴充套件性。

委託(delegate)有些書上叫代理或代表,都是一個意思,為了避免了另一個概念代理(Proxy)混淆,還是叫委託更好一些。

學過c++的人很熟悉指標,C#中沒有了指標,使用了委託,不同的是,委託是一個安全的型別,也是物件導向的。

2. 委託的使用

委託(delegate)的宣告的語法如下:

    public delegate void Del(string parameter);

 定義委託基本上是定義一個新類,所以可以在定義類的任何地方定義委託,既可以在另一個類的內部定義,也可以在任何類的外部定義,還可以在名稱空間中把委託定義為頂層物件。根據定義的可見性,可以在委託定義上新增一般的訪問修飾符:publicprivate、protected等:

實際上,“定義一個委託”是指“定義一個新類”。只是把class換成了delegate而已,委託實現為派生自基類System. Multicast Delegate的類,System.MulticastDelegate又派生自基類System.Delegate。

下面我們使用委託來實現上面動物園的例項,實現如下: 

 1 /// <summary>
 2 /// 動物類
 3 /// </summary>
 4 class Zoo
 5 {
 6     public class Manage
 7     {
 8         public delegate void Shout();   
 9         public static void CallAnimalShout(Shout shout)
10         {
11             shout();
12         }
13     }        
14     public class Dog
15     {
16         string name;
17         public Dog(string name)
18         {
19             this.name = name;
20         }
21         public void DogShout()            {
22 
23             Console.WriteLine("我是小狗:" + this.name + "汪~汪~汪");
24         }            
25     }
26     public class Sheep
27     {
28         string name;
29         public Sheep(string name)
30         {
31             this.name = name;
32         }
33         public void SheepShout()
34         {
35             Console.WriteLine("我是小羊:" + this.name + "咩~咩~咩");
36         }
37     }
38     public class Checken
39     {
40         string name;
41         public Checken(string name)
42         {
43             this.name = name;
44         }
45         public void ChickenShout()
46         {
47             Console.WriteLine("我是小雞:" + this.name + "喔~喔~喔");
48         }
49     }
50 }

動物園除了各種動物外,還有動物管理員,動物管理員有一個委託。呼叫如下:            

//參觀者委託管理員,讓某種動物叫
Zoo.Dog dog=new Zoo.Dog("汪財");
Zoo.Manage.Shout shout = new Zoo.Manage.Shout(dog.DogShout);
//管理員收到委託傳達給動物,動物執行主人命令
Zoo.Manage.CallAnimalShout(shout);        

執行結果如下:

 

上面的例項實現了委託的定義和呼叫,即間接的呼叫了動物叫的方法。肯定有人會說,為什麼不直接呼叫小狗叫的方法,而要繞一大圈來使用委託。如果只是簡單的讓一種動物叫一下,那麼用委託確實是繞了一大圈,但是如果我讓讓狗叫完,再讓羊叫,再讓雞叫,反反覆覆要了好幾種動物的叫聲,最後到如果要結算費用,誰能知道我消費了多少呢?如果一次讓幾種動物同時叫呢,我們是不是要再寫一個多個動物叫的方法來呼叫呢?當遇到複雜的呼叫時委託的作用就體現出來了,下面我們先看一下,如何讓多個動物同時叫,就是下面要說的多播委託。

委託需要滿足4個條件:

a.宣告一個委託型別
b.找到一個跟委託型別具有相同簽名的方法(可以是例項方法,也可以是靜態方法)
c.通過相同簽名的方法來建立一個委託例項
c.通過委託例項的呼叫完成對方法的呼叫 

3. 多播委託

每個委託都只包含一個方法呼叫,呼叫委託的次數與呼叫方法的次數相同。如果呼叫多個方法,就需要多次顯示呼叫這個委託。當然委託也可以包含多個方法,這種委託稱為多播委託。 

當呼叫多播委託時,它連續呼叫每個方法。在呼叫過程中,委託必須為同型別,返回型別一般為void,這樣才能將委託的單個例項合併為一個多播委託。如果委託具有返回值和/或輸出引數,它將返回最後呼叫的方法的返回值和引數。

下面我們看一下,呼叫“狗,雞,羊”同時叫的實現:           

//宣告委託型別
Zoo.Manage.Shout shout;
//加入狗叫委託
shout = new Zoo.Manage.Shout(new Zoo.Dog("小哈").DogShout);
//加入雞叫委託
shout += new Zoo.Manage.Shout(new Zoo.Checken("大鵬").ChickenShout);
//加入羊叫委託
shout += new Zoo.Manage.Shout(new Zoo.Sheep("三鹿").SheepShout);
//執行委託
Zoo.Manage.CallAnimalShout(shout);
Console.ReadLine();

執行結果如下:

  

上面的示例 ,多播委託用+=來新增委託,同樣可以使用 -=來移除委託

上面的示例,如果我們感覺還不足以體現委託的作用。我們假動物除了會叫之外,還有其它特技。狗會表演“撿東西(PickUp)”,羊會踢球(PlayBall),雞會跳舞(Dance)

觀眾想看一個集體表演了,讓狗叫1次,搶一個東西回來;羊叫1次踢1次球,雞叫1次跳1只舞。 然後,順序倒過來再表演一次。如果使用直接呼叫方法,那麼寫程式碼要瘋了,順序執行一次,就順序寫一排方法程式碼,要反過來表演,又要倒過來寫一排方法。這還不算高難度的表演,假如要穿插進行呢?使用委託的物件導向特徵,我們實現這些需求很簡單。看程式碼:

首先我們改進一下羊,狗,雞,讓他們有一個特技的方法。 

 1 /// <summary>
 2 /// 動物類
 3 /// </summary>
 4 class Zoo
 5 {
 6     public class Manage
 7     {
 8         public delegate void del();      
 9 
10         /// <summary>
11         /// 動物表演
12         /// </summary>
13         /// <param name="obj"></param>
14         /// <param name="shout"></param>
15         public static void CallAnimal(del d)
16         {
17             d();
18         }  
19     }
20     public  class Dog
21     {
22         string name;
23         public Dog(string name)
24         {
25             this.name = name;
26         }           
27         public void DogShout()
28         {
29             Console.WriteLine("我是小狗:"+this.name+"汪~汪~汪");
30         }      
31         public void PickUp()
32         {
33             Console.WriteLine("小狗" + this.name + " 撿東西 回來了");
34         }
35     }
36     public class Sheep
37     {
38         string name;
39         public Sheep(string name)
40         {
41             this.name = name;
42         }
43         public void SheepShout()
44         {
45             Console.WriteLine( "我是小羊:"+this.name+" 咩~咩~咩 ");
46         }
47         public void PlayBall() 
48         {
49             Console.WriteLine("小羊" + this.name + " 打球 結束了");
50         }
51     }
52 
53     public class Chicken
54     {
55             string name;
56             public Chicken(string name)
57         {
58             this.name = name;
59         }
60         public void ChickenShout()
61         {
62             Console.WriteLine("我是小雞:"+this.name+"喔~喔~喔");
63         }
64         public void Dance()
65         {
66             Console.WriteLine("小雞" + this.name + " 跳舞 完畢");
67         }
68     }
69 }

 呼叫如下: 

 1 //多播委託(二)動物狂歡
 2 
 3 //挑選三個表演的動物
 4 Zoo.Dog dog = new Zoo.Dog("小哈");
 5 Zoo.Chicken chicken = new Zoo.Chicken("大鵬");
 6 Zoo.Sheep sheep = new Zoo.Sheep("三鹿");
 7 
 8 //加入狗叫委託
 9 Zoo.Manage.del dogShout = dog.DogShout;
10 //加入雞叫委託
11 Zoo.Manage.del chickenShout = chicken.ChickenShout;
12 //加入羊叫委託
13 Zoo.Manage.del sheepnShout = sheep.SheepShout;
14 
15 //加入狗表演
16 Zoo.Manage.del dogShow = new Zoo.Manage.del(dog.PickUp);
17 //加入雞表演
18 Zoo.Manage.del chickenShow = new Zoo.Manage.del(chicken.Dance);
19 //加入羊表演
20 Zoo.Manage.del sheepShow = new Zoo.Manage.del(sheep.PlayBall);
21 
22 
23 //構造表演模式
24 //第一種表演方式:狗叫1次搶一個東西回來;羊叫1次踢1次球;雞叫1次跳1只舞;
25 Zoo.Manage.del del = dogShout + dogShow + chickenShout + chickenShow + sheepnShout + sheepShow;
26 //執行委託
27 Zoo.Manage.CallAnimal(del);
28 
29 
30 Console.WriteLine("\n第二種表演,順序反轉\n");
31 //第二種表演,順序反轉
32 var del2 = del.GetInvocationList().Reverse();
33 //執行委託
34 foreach (Zoo.Manage.del d in del2)           
35 Zoo.Manage.CallAnimal(d);
36 Console.ReadLine();

執行結果如下:

 

使用多播委託有兩點要注意的地方:

(1)多播委託的方法並沒有明確定義其順序,儘量避免在對方法順序特別依賴的時候使用。

(2)多播委託在呼叫過程中,其中一個方法丟擲異常,則整個委託停止。

4. 匿名方法

我們通常都都顯式定義了一個方法,以便委託呼叫,有一種特殊的方法,可以直接定義在委託例項的區塊裡面。我們在LINQ基礎一節中,已經舉例說明過匿名方法。例項化普通方法的委託和匿名方法的委託有一點差別。下面我們看一下示例:

//定義委託
delegate void Add(int a,int b);
//例項委託,使用匿名方法
Add add = delegate(int a, int b)
{
    Console.WriteLine(a + "+" + b + "=" + (a + b));
};

//呼叫
add(1, 2);
add(11, 32);

返回結果為: 1+2=3  11+32=43

4.1 對於匿名方法有幾點注意:

(1)在匿名方法中不能使用跳轉語句調到該匿名方法的外部;反之亦然:匿名方法外部的跳轉語句不能調到該匿名方法的內部。

(2)在匿名方法內部不能訪問不完全的程式碼。

(3)不能訪問在匿名方法外部使用的refout引數,但可以使用在匿名方法外部定義的其他變數。

(4)如果需要用匿名方法多次編寫同一個功能,就不要使用匿名方法,而編寫一個指定的方法比較好,因為該方法只能編寫一次,以後可通過名稱引用它。

4.2 匿名方法的適用環境:

1)在呼叫上下文中的變數時

2)該方法只呼叫一次時,如果方法在外部需要多次呼叫,建議使用顯示定義一個方法.

       可見,匿名方法是一個輕量級的寫法。 

4.3 使用Labmda表示式書寫匿名方法

Linq基礎一節中,我們說了,Labmda表示式是基於數學中的λ(希臘第11個字母)演算得名,而“Lambda 表示式”(lambda expression)是指用一種簡單的方法書寫匿名方法。

上面的匿名方法,我們可以使用等效的Labmda表示式來書寫,如下:

//使用Lambda表示式的匿名方法 例項化並呼叫委託
Add add2 = (a, b) => { Console.WriteLine(a + "+" + b + "=" + (a + b)); };
add2(3, 4);
add2(3, 31);

//返回結果為:3+4=7 3+31=34

“=>”符號左邊為表示式的引數列表,右邊則是表示式體(body)。引數列表可以包含0到多個引數,引數之間使用逗號分割。

5. 泛型委託

前面我們說了通常情況下委託的宣告及使用,除此之外,還有泛型委託

泛型委託一共有三種:

Action(無返回值泛型委託)

Func(有返回值泛型委託)

predicate(返回值為bool型的泛型委託)

下面一一舉例說明

5.1  Action(無返回值泛型委託)

示例如下: 

 1         /// <summary>
 2         /// 提供委託簽名方法
 3         /// </summary>
 4         /// <typeparam name="T"></typeparam>
 5         /// <param name="action"></param>
 6         /// <param name="a"></param>
 7         /// <param name="b"></param>
 8         static void ActionAdd<T>(Action<T,T> action,T a,T b)
 9         {
10             action(a,b);
11         }
12 
13         //兩個被呼叫方法
14        static  void Add(int a,int b)
15         {
16             Console.WriteLine(a + "+" + b + "=" + (a + b));
17         }
18 
19        static void Add(int a, int b,int c)
20         {
21             Console.WriteLine(a + "+" + b + "+"+c+"=" + (a + b));
22         }

 宣告及呼叫如下:

//普通方式呼叫
ActionAdd<int>(Add,1,2);

//匿名方法宣告及呼叫
Action<int,int> acc = delegate(int a,int b){
    Console.WriteLine(a + "+" + b + "=" + (a + b)); 
};
acc(11, 22);

//表示式宣告及呼叫
Action<int, int> ac = (a,b)=>{ Console.WriteLine(a + "+" + b + "=" + (a + b)); };
ac(111, 222);

 返回值如下:

可以使用 Action<T1, T2, T3, T4> 委託以引數形式傳遞方法,而不用顯式宣告自定義的委託。 

封裝的方法必須與此委託定義的方法簽名相對應。 也就是說,封裝的方法必須具有四個均通過值傳遞給它的引數,並且不能返回值。

(在 C# 中,該方法必須返回 void)通常,這種方法用於執行某個操作。

 5.2 Func(有返回值泛型委託)

示例如下:

 1 /// <summary>
 2 /// 提供委託簽名方法
 3 /// </summary>
 4 /// <typeparam name="T"></typeparam>
 5 /// <param name="action"></param>
 6 /// <param name="a"></param>
 7 /// <param name="b"></param>
 8 static string  FuncAdd<T,T2>(Func<T,T2,string> func,T a,T2 b)
 9 {
10     return func(a,b);
11 }
12 
13 //兩個被呼叫方法
14 static  string  Add(int a,int b)
15 {
16     return (a + "+" + b + "=" + (a + b));
17 }

呼叫如下:

//有返回值的泛型委託Func

//普通方式呼叫
Console.WriteLine(FuncAdd<int,int>(Add, 1, 2));
//匿名方法宣告及呼叫
Func<int,int,string> acc = delegate(int a,int b){
   return (a + "+" + b + "=" + (a + b)); 
}; 
Console.WriteLine(acc(11, 22));
//表示式宣告及呼叫
Func<int, int,string> ac = (a, b) => {return (a + "+" + b + "=" + (a + b)); };
Console.WriteLine(ac(111, 222));

執行結果同上例

5.3 predicate(返回值為bool型的泛型委託)

表示定義一組條件並確定指定物件是否符合這些條件的方法。此委託由 Array 和 List 類的幾種方法使用,用於在集合中搜尋元素。

使用MSDN官方的示例如下 : 

 1 //以下示例需要引用System.Drawing程式集
 2 private static bool ProductGT10( System.Drawing.Point p)
 3 {
 4     if (p.X * p.Y > 100000)
 5     {
 6         return true;
 7     }
 8     else
 9     {
10         return false;
11     }
12 }

 呼叫及執行結果如下:

System.Drawing.Point[] points = { new  System.Drawing.Point(100, 200), 
    new  System.Drawing.Point(150, 250), new  System.Drawing.Point(250, 375), 
    new  System.Drawing.Point(275, 395), new  System.Drawing.Point(295, 450) };
System.Drawing.Point first = Array.Find(points, ProductGT10);
Console.WriteLine("Found: X = {0}, Y = {1}", first.X, first.Y);
Console.ReadKey();
            
//輸出結果為:
//Found: X = 275, Y = 395

6.委託中的協變和逆變

將方法簽名與委託型別匹配時,協變和逆變為您提供了一定程度的靈活性。協變允許方法具有的派生返回型別比委託中定義的更多。逆變允許方法具有的派生引數型別比委託型別中的更少

關於協變和逆變要從物件導向繼承說起。繼承關係是指子類和父類之間的關係;子類從父類繼承所以子類的例項也就是父類的例項。比如說Animal是父類,Dog是從Animal繼承的子類;如果一個物件的型別是Dog,那麼他必然是Animal。

協變逆變正是利用繼承關係 對不同引數型別或返回值型別 的委託或者泛型介面之間做轉變。我承認這句話很繞,如果你也覺得繞不妨往下看看。

如果一個方法要接受Dog引數,那麼另一個接受Animal引數的方法肯定也可以接受這個方法的引數,這是Animal向Dog方向的轉變是逆變。如果一個方法要求的返回值是Animal,那麼返回Dog的方法肯定是可以滿足其返回值要求的,這是Dog向Animal方向的轉變是協變。

由子類向父類方向轉變是協變 協變用於返回值型別用out關鍵字
由父類向子類方向轉變是逆變 逆變用於方法的引數型別用in關鍵字

協變逆變中的協逆是相對於繼承關係的繼承鏈方向而言的。

6.1  陣列的協變:

Animal[] animalArray = new Dog[]{};

上面一行程式碼是合法的,宣告的陣列資料型別是Animal,而實際上賦值時給的是Dog陣列;每一個Dog物件都可以安全的轉變為Animal。Dog向Animal方法轉變是沿著繼承鏈向上轉變的所以是協變

6.2  委託中的協變和逆變

6.2.1 委託中的協變

//委託定義的返回值是Animal型別是父類
public delegate Animal GetAnimal();
//委託方法實現中的返回值是Dog,是子類
static Dog GetDog(){return new Dog();}
//GetDog的返回值是Dog, Dog是Animal的子類;返回一個Dog肯定就相當於返回了一個Animal;所以下面對委託的賦值是有效的
GetAnimal getMethod = GetDog;

6.2.2  委託中的逆變

//委託中的定義引數型別是Dog
public delegate void FeedDog(Dog target);
//實際方法中的引數型別是Animal
static void FeedAnimal(Animal target){}
// FeedAnimal是FeedDog委託的有效方法,因為委託接受的引數型別是Dog;而FeedAnimal接受的引數是animal,Dog是可以隱式轉變成Animal的,所以委託可以安全的的做型別轉換,正確的執行委託方法;
FeedDog feedDogMethod = FeedAnimal;

定義委託時的引數是子類,實際上委託方法的引數是更寬泛的父類Animal,是父類向子類方向轉變,是逆變

6.3  泛型委託的協變和逆變: 

6.3.1 泛型委託中的逆變
如下委託宣告:

public delegate void Feed<in T>(T target)

Feed委託接受一個泛型型別T,注意在泛型的尖括號中有一個in關鍵字,這個關鍵字的作用是告訴編譯器在對委託賦值時型別T可能要做逆變

/先宣告一個T為Animal的委託
Feed<Animal> feedAnimalMethod = a=>Console.WriteLine(“Feed animal lambda”);
//將T為Animal的委託賦值給T為Dog的委託變數,這是合法的,因為在定義泛型委託時有in關鍵字,如果把in關鍵字去掉,編譯器會認為不合法
Feed<Dog> feedDogMethod = feedAnimalMethod;

6.3.2 泛型委託中的協變 

如下委託宣告:

public delegate T Find<out T>();

Find委託要返回一個泛型型別T的例項,在泛型的尖括號中有一個out關鍵字,該關鍵字表明T型別是可能要做協變的

//宣告Find<Dog>委託
Find<Dog> findDog = ()=>new Dog();
 
//宣告Find<Animal>委託,並將findDog賦值給findAnimal是合法的,型別T從Dog向Animal轉變是協變
Find<Animal> findAnimal = findDog;

6.4 泛型介面中的協變和逆變: 

泛型介面中的協變逆變和泛型委託中的非常類似,只是將泛型定義的尖括號部分換到了介面的定義上。
6.4.1 泛型介面中的逆變
如下介面定義:

public interface IFeedable<in T>
{
    void Feed(T t);
}

介面的泛型T之前有一個in關鍵字,來表明這個泛型介面可能要做逆變 

如下泛型型別FeedImp<T>,實現上面的泛型介面;需要注意的是協變和逆變關鍵字in,out是不能在泛型類中使用的,編譯器不允許

public class FeedImp<T>:IFeedable<T>
{
    public void Feed(T t){
        Console.WriteLine(“Feed Animal”);
    }
}

來看一個使用介面逆變的例子:

IFeedable<Dog> feedDog = new FeedImp<Animal>();

上面的程式碼將FeedImp<Animal>型別賦值給了IFeedable<Dog>的變數;Animal向Dog轉變了,所以是逆變 

6.4.2 泛型介面中的協變
如下介面的定義:

public interface IFinder<out T>
{
    T Find();
}

泛型介面的泛型T之前用了out關鍵字來說明此介面是可能要做協變的;如下泛型介面實現類

public class Finder<T>:IFinder<T> where T:new()
{
    public T Find(){
        return new T();
    }
}

//使用協變,IFinder的泛型型別是Animal,但是由於有out關鍵字,我可以將Finder<Dog>賦值給它

Finder<Animal> finder = new Finder<Dog>();

協變和逆變的概念不太容易理解,可以通過實際程式碼思考理解。這麼繞的東西到底有用嗎?答案是肯定的,通過協變和逆變可以更好的複用程式碼。複用是軟體開發的一個永恆的追求。

7. 要點

7.1 委託的返回值及引數總結

  (1Delegate至少0個引數,至多32個引數,可以無返回值,也可以指定返回值型別

  (2Func可以接受0個至16個傳入引數,必須具有返回值 

  (3Action可以接受0個至16個傳入引數,無返回值

  (4Predicate只能接受一個傳入引數,返回值為bool型別

7.2 委託的幾種寫法總結:

1)、委託 委託名=new 委託(會呼叫的方法名); 委託名(引數);

2)、委託 委託名 =會呼叫的方法名委託名(引數);

3)、匿名方法

委託 委託名=delegate(引數){會呼叫的方法體};委託名(引數);

4)、拉姆達表示式

委託 委託名=((引數1,。。引數n=>{會呼叫的方法體});委託名(引數);

5)、用Action<T>Func<T>,第一個無返回值

Func<引數1, 引數2, 返回值委託名= ((引數1,引數2) => {帶返回值的方法體 });返回值=委託名(引數1,引數2);

7.3.重要的事情說三遍:
1委託”delegate)(代表、代理):是型別安全的並且完全物件導向的。在C#中,所有的代理都是從System.Delegate類派生的(delegateSystem.Delegate的別名)。
2委託隱含具有sealed屬性,即不能用來派生新的型別。
3委託最大的作用就是為類的事件繫結事件處理程式。
4)在通過委託呼叫函式前,必須先檢查委託是否為空(null),若非空,才能呼叫函式。

(5)委託理例項中可以封裝靜態的方法也可以封裝例項方法。
6)在建立委託例項時,需要傳遞將要對映的方法或其他委託例項以指明委託將要封裝的函式原型(.NET中稱為方法簽名:signature)。注意,如果對映的是靜態方法,傳遞的引數應該是類名.方法名,如果對映的是例項方法,傳遞的引數應該是例項名.方法名。
7)只有當兩個委託例項所對映的方法以及該方法所屬的物件都相同時,才認為它們是相等的(從函式地址考慮)。
8)多個委託例項可以形成一個委託鏈,System.Delegate中定義了用來維護委託鏈的靜態方法CombionRemove,分別向委託鏈中新增委託例項和刪除委託例項。
9委託三步曲:
      a.生成自定義委託類:delegate int MyDelegate();
      b.然後例項化委託類:MyDelegate d = new MyDelegate(MyClass.MyMethod);
      c.最後通過例項物件呼叫方法:int ret = d()

10)委託的返回值通常是void,雖然不是必須的,但是委託允許定義多個委託方法(即多播委託),設想他們都有返回值,最後返回的值會覆蓋前面的,因此通常都定義為void.

 ============================================================================================== 

返回目錄

 <如果對你有幫助,記得點一下推薦哦,有不明白的地方或寫的不對的地方,請多交流>
 

QQ群:467189533

==============================================================================================  

相關文章