C# 委托和事件 與 觀察者模式(發布-訂閱模式)講解 by天命


使用面向對象的思想 用c#控制台代碼模擬貓抓老鼠

 

我們先來分析一下貓抓老鼠的過程

1.貓叫了

2.所有老鼠聽到叫聲,知道是哪只貓來了

3.老鼠們逃跑,邊逃邊喊:"xx貓來了,快跑啊!我是老鼠xxx"

 

一  雙向耦合的代碼

 

首先需要一個貓類Cat 一個老鼠類Rat 和一個測試類Program

老鼠類的代碼如下

//老鼠類
public class Rat
{
    public string Name { get; set; } //老鼠的名字
    public Cat MyCat { get; set; } //老鼠遇到的貓

    //老鼠逃跑的方法
    public void Run()
    {
        Console.WriteLine(MyCat.Name +
            "貓來了,大家快跑!!我是" + Name);
        //打印出信息 包含了貓的名字和老鼠本身的名字
    }

    //帶參和無參構造
    public Rat() { }
    public Rat(string name, Cat cat)
    {
        this.Name = name;
        this.MyCat = cat;
    }
}

 

要讓貓叫的時候依次打印出老鼠的逃跑方法,需要在Cat類里添加一個存放Rat對象的集合

Cat類的代碼如下

public class Cat
{
    public string Name { get; set; } //貓的名字
    List<Rat> list = new List<Rat>(); //存放Rat對象的集合

    //為集合增加老鼠
    public void Add(Rat rat)
    {
        list.Add(rat);
    }

    //移除
    public void Remove(Rat rat)
    {
        list.Remove(rat);
    }

    //貓叫的方法
    public void Shout()
    {
        Console.WriteLine("喵喵喵!");
        //如果集合非空,循環執行每只老鼠的Run()方法
        if (list != null)
        {
            foreach (Rat item in list)
            {
                item.Run();
            }
        }
    }

    public Cat() { }
    public Cat(string name)
    {
        this.Name = name;
    }
}

 

在Main方法中,我們需要構建幾個Rat對象和一個Cat對象,將Rat對象添加到Cat對象的集合中,調用Cat對象的Shout方法

代碼如下

static void Main(string[] args)
{
    //構建一個Cat對象和兩個Rat對象 老鼠遇到的貓是構建的cat
    Cat cat = new Cat("Tom");
    Rat rat1 = new Rat("Jerry", cat);
    Rat rat2 = new Rat("TaoQi", cat);

    //調用貓類的Add方法添加老鼠對象
    cat.Add(rat1);
    cat.Add(rat2);

    //調用貓的Shout方法
    cat.Shout();

    Console.ReadKey();
}

 

運行結果如下

 

 

 

這樣的代碼缺陷很明顯,Cat類和Rat類緊密耦合

貓可能不止要抓老鼠 還要抓小鳥

當然不止是貓會抓 也可能狗拿耗子多管閑事

於是我們可以把貓和狗提出來 繼承自一個抽象類Pet

抓捕的小動物老鼠和小鳥沒有什么關系 但是都能(逃)跑

先不去管小鳥是飛,我們把它們稱作 可以跑的 都實現一個IRunable接口

 

 

二  觀察者 模式(發布-訂閱模式)

 

修改后的代碼如下

新增抽象類Pet ,貓類繼承自Pet   (貓類的代碼變化不大 略去不寫

 

public abstract class Pet
{
    public List<IRunable> list = new List<IRunable>();
    public void Add(IRunable i)
    {
        list.Add(i);
    }

    public void Remove(IRunable i)
    {
        list.Remove(i);
    }

    public abstract void Shout();
}

 

 

接口IRubable 里面定義一個Run方法

public interface IRunable
{
void Run();
}

 

老鼠Rat和鳥Bird兩個類都實現了這個接口

以Bird為例 代碼如下

class Bird : IRunable
{
    //鳥的名字和遇到的貓
    public string Name { get; set; }
    public Cat MyCat { get; set; }

    public void Run()
    {
        Console.WriteLine(MyCat.Name + "貓來了,快跑吧!我是小鳥" + Name);
    }

    public Bird() { }
    public Bird(string name, Cat cat)
    {
        this.Name = name;
        this.MyCat = cat;
    }
}

 

 

Rat類的代碼幾乎沒有變化

那么在Main方法中也只需要稍作修改,增加一個Bird對象 略去不寫 執行后的結果如下

 

 

 以上貓抓老鼠的例子實際上就是用了一個設計模式:觀察者模式

觀察者模式又名發布-訂閱模式(Publish-Subscribe)

 

1.Subject類 (通知者 主題)
//抽象類 里面有一個Observer類集合
把所有對觀察者對象的引用保存在一個聚集里,每個主題都可以有多個觀察者.抽象主題提供一個接口,可以增加和刪除觀察着對象

2.Observer類 (觀察者)
//抽象類
抽象觀察者,為所有的具體觀察者定義一個接口,在得到主題的更新時提醒自己

3.ConcreteObserver類
//父類是Observer類
具體觀察者,實現抽象觀察者角色所需求的更新接口,以便使本身的狀態與主題的狀態相協調

4.ConcreteSubject類
//父類是Subject
具體主題,將有關狀態存入具體觀察者對象,在具體主題的內部狀態改變時,給所有登記過的觀察者發出通知

 

 

觀察者模式的特點

 

1.一個主題可以有任意數量依賴他的觀察者,一旦主題的狀態發生改變,所有觀察者都可以得到通知
2.主題發出通知不需要知道具體觀察者
3.具體觀察者也不需要知道其他觀察者的存在

 

但是

將一個系統分割成一系列相互作用的類有一個很不好的副作用,就是需要維護相關對象間的一致性,使得各個類緊密耦合,這樣會給維護,擴展和重用都帶來不便

 

應用時機:

當一個對象的改變需要同時改變其他對象的時候使用觀察模式
不知道具體有多少對象有待改變時,應考慮使用觀察者模式
一個抽象模型有兩個方面,其中一個方面依賴於另一個方面

 

 

三 委托

 

舉個栗子 正如之前所說,老鼠會跑Run,小鳥會飛Fly,這根本是兩個毫不相干的方法

但是的確有相同點--它們的返回值類型都是空,傳進的參數列表也都為空

 

我們怎么樣能把這兩個不相關的類Bird和Rat的對象都裝到Cat中去,再判斷是哪個類依次調用它們的方法?

其實我們可以直接拿出它們的方法來裝到Cat中去.

 

 

//委托
委托是什么?
和類一樣是一種用戶自定義類型
委托提供了方法的抽象

 

委托存儲的是一系列具有相同簽名和返回值類型的方法的地址,調用委托的時候,委托包含的所有方法將被執行

 

1.定義委托
訪問修飾符 delegate 返回值類型 委托名(參數列表);

public delegate void MyDel(int x);

 

 

2.聲明和初始化
委托名 委托對象名;
委托對象名=new 委托名(類名.方法名);

MyDel del;
del=new MyDel(Cat.Shout);

 

委托名=方法名
del=Cat.Shout;

 

3.委托的運算
委托可以使用額外的運算符來組合.這個運算最終會創建一個新的委托,其調用列表是兩個操作數的委托調用列表的副本連接.委托是恆定的,操作數委托創建后不會被改變,委托組合拷貝的是操作數的副本
MyDel del2=Cat.Catch;
MyDel del3=del+del2;

 

使用+=為委托新增方法
使用-=為委托移除方法
del+=Cat.Catch;
del-=Cat.Shout;

 

4.委托調用

if(del!=null){
    del();//委托調用
}

 

 

5.匿名方法
匿名方法是在初始化委托時內聯聲明的方法
delegate(參數){代碼塊}

delegate int MyDel(int x);
MyDel del=delegate(int x){
    return x;
};

//匿名方法不聲明返回值

 

Lambda表達式
用來簡化匿名方法的語法

MyDel del1 = (int x) => {return x;};

在參數列表和匿名方法之間放置Lambda運算符=>
Lambda運算符讀作 goes to

MyDel del1 = x => {return x};
MyDel del1 = x => x;

 

 

定義委托像定義枚舉一樣,可以定義在類的外部

然后在類中可以創建委托的對象

比如 修改后的我們的程序

(只是一個測試 不寫的很詳細了 還是拿貓和老鼠舉例子 貓類和老鼠類 以及定義的委托的代碼如下)

 

//定義一個委托 名叫CatShoutEventHandler 沒有返回值 參數列表為空
public delegate void CatShoutEventHandler();

//貓類
public class Cat
{
    //在貓類里定義一個該委托的對象CatShout
    public CatShoutEventHandler CatShout;

    public void Shout()
    {
        Console.WriteLine("喵喵喵");
        //判斷委托內是否為空,若不為空,執行該委托
        if (CatShout != null)
        {
            CatShout();
        }
    }
}

//老鼠類
public class Rat
{
    public string Name { get; set; } //老鼠的名字

    //老鼠的逃跑方法
    public void Run()
    {
        Console.WriteLine(Name + "跑了!");
    }

    //無參和帶參構造
    public Rat() { }
    public Rat(string name)
    {
        this.Name = name;
    }
}

 

那么接下來就是我們的Main方法

static void Main(string[] args)
{
    //構建一個貓對象和兩個老鼠對象
    Cat cat = new Cat();
    Rat rat1 = new Rat("Jerry");
    Rat rat2 = new Rat("TaoQi");

    //向cat的委托對象CatShout中依次添加老鼠對象的Run方法
    //注!!添加的是整個方法 不需要加括號
    cat.CatShout += rat1.Run;
    cat.CatShout += rat2.Run;

    //調用cat的Shout方法
    cat.Shout();
    Console.ReadKey();
}

 

運行結果是這樣的

 

 

然而 然而 然而

比如說我來做一個很賤的操作

在Main方法中來一個 cat.CatShout=null;

好了 不管Cat類中的CatShout有沒有初始值或者有沒有賦值過都沒了

 

我們知道面向對象的的三大特征 封裝,繼承,多態

我們一樣可以把委托封裝起來,以控制它的訪問權限

這就是事件

 

 

四 事件

 

事件(Event)是一個用戶操作,或是一些特定的出現情況.應用程序需要在事件發生時響應事件

事件在類中聲明且生成,且通過使用同一個類或者其他類中的委托與時間處理程序關聯

 

聲明事件
在類的內部聲明事件,首先必須聲明該事件的委托類型

public delegate void CatShoutEventHandler();

然后聲明事件本身,使用 event 關鍵字

public event CatShoutEventHandler CatShout;

上面的代碼定義了一個名為CatShoutEventHandler的委托和一個CatShout的事件,該事件在生成時會調用委托

聲明事件后可以實例化事件,注冊函數到事件解除事件函數注冊方法

CatShout+=new CatShoutEventHandler(rat.Run);
CatShout+=rat2.Run;//將函數Run注冊到事件CatSHout上

 在使用事件的時候,如果在封裝類的外部,則該事件只能出現在+=或-=的左邊

 

所以 用事件封裝過的代碼我們再來一遍

 

額..好像順理成章的就寫了下去

代碼就這里變了一行 唯一的變化就是多了個event關鍵字

我們在這里使用了一個高大上的工具 reflector

將我們的代碼反編譯之后,可以看到CatShout這個事件其實做了兩件事

里面有兩個方法 一個是add_CatShout(CatShoutEventHandler)  另一個是remov_Cat(CatShoutEventHandler)

分別對應這個事件的運算符號 +=  和 -=

所以其實事件就是相當於對委托的封裝

 

 

 

五 Object sender和EventArgs e

 

 

然而我又要找問題了,我們翻回去看要求

有一點是老鼠知道貓的名字,要調用貓對象的Name屬性,我們現在試着給貓加上這個屬性

public string Name { get; set; }

我們要排除Cat類與Rat類的耦合,所以不能在Rat類中存放一個Cat對象

當然我們可以在老鼠的Run方法中增加傳進去一個Cat對象,但是這樣需要定義一個這個程序自己使用的委托類型

 

系統已經有一些定義好的委托類型

public delegate void Action();
public delegate void EventHandler(object sender, EventArgs e);

第一個委托叫Action 它是沒有參數 沒有返回值類型的

第二個叫做EventHandler 它有兩個參數 Object類型的sender 和 EventArgs類型的e

第一個還好,第二個,我去,這是什么東西啊?

其實第二個這個委托類型我們都十分熟悉 因為在winForm窗體應用程序中,控件生成的方法就帶着這兩個參數 

我們一般把觸發事件的整個對象封裝成Object類型 做第一個參數

而第二個參數呢 我們首先需要知道什么是EventArgs

 

    [Serializable, ComVisible(true), __DynamicallyInvokable]
    public class EventArgs
    {
        [__DynamicallyInvokable]
        public static readonly EventArgs Empty = new EventArgs();
    }

 

這是代碼 首先我們知道了,這是個類

是系統定義的類,里面只有一個靜態的readonly的自身變量 為空

也就是通過調用這個靜態方法來返回一個新的(空的)EventArgs本身

 

要弄懂它,我們來先看一個例子

這是WinForm窗體中的TreeView的事件對應在窗體類中生成的方法

 private void tvList_AfterSelect(object sender, TreeViewEventArgs e)

我們可以看到 誒 誒 里面傳進去的第一個參數沒錯是object sender 然而第二個變成了TreeViewEventArgs類型的e

那這個TreeViewEventArgs是什么東西呢

首先沒錯的是它繼承自EventArgs

然后它加多了許多方法,我們知道的是它可以返回我們當前選中的那個節點(而sender則代表的是整個TreeView控件)

sa,我們可以把要用到的一些屬性封裝到EventArgs之中 來排除兩個類的耦合

 

應用到貓和鼠的例子中去,我們最后的代碼是這樣子的

 

//自定義的CatShoutEventArgs類,繼承自EventArgs
//用作保存Cat對象的Name屬性,還可以擴展其他的功能
public class CatShoutEventArgs : EventArgs
{
    public string CatName { get; set; }
    public CatShoutEventArgs(string name)
    {
        this.CatName = name;
    }
}

//定義一個委托 名叫CatShoutEventHandler 
public delegate void CatShoutEventHandler
            (object sender,CatShoutEventArgs e);

//貓類
public class Cat
{
    public string Name { get; set; } //貓的名字

    //在貓類里定義一個事件CatShout,返回值類型是定義的委托
    public event CatShoutEventHandler CatShout;

    public void Shout()
    {
        Console.WriteLine("喵喵喵");
        //判斷委托內是否為空,若不為空,執行該委托
        if (CatShout != null)
        {
            //new一個CatShoutEventArgs類,傳入參數是自身的Name
            CatShoutEventArgs e = new CatShoutEventArgs(Name);
            //執行CatShout事件,傳入自身和e
            CatShout(this, e);
        }
    }

    //無參和帶參構造
    public Cat() { }
    public Cat(string name)
    {
        this.Name = name;
    }
}

//老鼠類
public class Rat
{
    public string Name { get; set; } //老鼠的名字

    //老鼠的逃跑方法
    public void Run(object sender, CatShoutEventArgs e)
    {
        //打出一句話,包括了貓的名字和老鼠的名字
        Console.WriteLine(e.CatName + "來了! " + Name + "跑了!");
    }

    //無參和帶參構造
    public Rat() { }
    public Rat(string name)
    {
        this.Name = name;
    }
}

嗯 然后Program類中的Main方法並不需要改動

為了視覺效果,我再放上來一遍

static void Main(string[] args)
{
    //構建一個貓對象和兩個老鼠對象
    Cat cat = new Cat("Tom");
    Rat rat1 = new Rat("Jerry");
    Rat rat2 = new Rat("TaoQi");

    //向cat的委托對象CatShout中依次添加老鼠對象的Run方法
    //注!!添加的是整個方法 不需要加括號
    cat.CatShout += rat1.Run;
    cat.CatShout += rat2.Run;

    //調用cat的Shout方法
    cat.Shout();
    Console.ReadKey();
}

 

好了 好了 最后的最后 看看我們程序的運行效果吧

 

 

by天命 2016.11.9


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM