簡體   English   中英

C#事件的實現

[英]C# Implementation of Event

我正在學習關於類的C#事件實現。

我有例子:

Car class繼承了Sport和City課程。 Car class具有由Sport和City類繼承的基本方法調用OnBuy。 在OnBuy方法中,事件處理程序已分配給Event Buy。

還有一個名為LicenseService的服務或類,每次購買時都會生成許可證號。

在這種情況下,我實現了事件驅動編程。 這是我的git示例:

https://github.com/adityosnrost/CSharpLearningEvent

問題:

  1. 這是在C#上使用Event的正確方法嗎?

  2. 如果這是正確的。 我可以將OnBuy方法覆蓋到每個孩子身上嗎? 如果覆蓋可用,我該怎么辦?

  3. 我可以做些什么來使這個樣本更好?

謝謝

class Program
{
    static void Main(string[] args)
    {
        Car car = new Car();
        Sport sport = new Sport();
        City city = new City();

        //car.Name();
        //sport.Name();
        //city.Name();

        //Console.ReadLine();

        LicenseService ls = new LicenseService();

        city.Buy += ls.GenerateLicense;

        city.OnBuy();

        Console.ReadLine();
    }
}

internal class Car
{
    internal virtual void Name()
    {
        Console.WriteLine("Car");
    }

    internal event EventHandler Buy;

    internal virtual void OnBuy()
    {
        EventHandler handler = Buy;
        if (null != handler)
        {
            handler(this, EventArgs.Empty);
        }
    }
}

internal class Sport: Car
{
    internal override void Name()
    {
        Console.WriteLine("Sport");
    }
}

internal class City: Car
{
    internal override void Name()
    {
        Console.WriteLine("City");
    }
}

internal class LicenseService
{
    internal void GenerateLicense(object sender, EventArgs args)
    {
        Random rnd = new Random();

        string carType = sender.GetType().ToString();

        string licenseNumber = "";

        for(int i = 0; i < 5; i++)
        {
            licenseNumber += rnd.Next(0, 9).ToString();
        }

        Console.WriteLine("{1} Car has been bought, this is the license number: {0}", licenseNumber, carType);
    } 
}

如果我正在制作這個節目,我會做出以下更改,

  • 活動簽名

首先它有兩個參數ObjectEventArgs ,你只需要Car in handler方法(以及隨后的Random討論原因)。

  • 我會在Child的構造函數中傳遞LicenseService ,並LicenseService在構造函數中注冊(subscribe)Event。 那將是更清潔的方式。
  • 會在父類中創建一個字符串成員CarName ,這樣每個孩子都可以在任何他們想要的地方使用它。
  • 還有一件事,我沒有在這段代碼中做過,我永遠不會命名一個事件Buy ,而是我會命名為Bought
  • (這僅適用於此場景)在您的代碼中, GenerateLicense()內部每次都會創建Random新對象。 因此,如果您對該方法的兩次調用都在很短的時間內,它將為兩個調用生成相同的隨機數。 為什么? 看到這個問題 - 或者您可以自己嘗試下面的示例代碼。 所以我會在GenerateLicense()傳遞已經創建的Random對象。 因此,對於該方法的每次調用, Random都是常見的。

用於解釋隨機數行為的示例代碼

        //as object of Random numbers are different,
        //they will generate same numbers
        Random r1 = new Random();
        for(int i = 0; i < 5; i++)
            Console.WriteLine(r1.Next(0, 9));
        Random r2 = new Random();
        for(int i = 0; i < 5; i++)
            Console.WriteLine(r2.Next(0, 9));

更新

  • 正如Mrinal Kamboj所建議的那樣(在下面的評論中),我們不應該將Events暴露給外部代碼。 在這個答案中添加他的評論

兩點, EventHandler Buy不允許直接訪問外部它應該是私有的,因為任何人都可以將其設置為null並且所有訂閱都消失了。 它需要一個事件訪問器,因此可以使用+=-=運算符訪問事件,並且它本身可以使多個訂戶的線程安全,否則會出現競爭條件,請查看一個簡單的示例

以下是代碼,

你的班級結構:

internal delegate void EventHandler(Car car, Random rnd);
internal class Car
{
    internal string CarName;
    internal virtual void SetName()
    {
        this.CarName = "car";
    }

    //Edit : As Mrinal Kamboj suggested in comments below
    //here keeping event Buy as private will prevent it to be used from external code
    private event EventHandler Buy;
    //while having EventAccessros internal (or public) will expose the way to subscribe/unsubscribe it
    internal event EventHandler BuyAccessor
    {
        add 
        {
            lock (this)
            {
                Buy += value;
            }
        }
        remove
        {
            lock (this)
            {
                Buy -= value;
            }
        }
    }

    internal virtual void OnBuy(Random rnd)
    {
        if (Buy != null)
            Buy(this, rnd);
    }
}

internal class Sport: Car
{
    LicenseService m_ls;
    internal Sport(LicenseService ls)
    {
        this.m_ls = ls;
        this.BuyAccessor += ls.GenerateLicense;
        SetName();
    }

    internal override void SetName()
    {
        this.CarName = "Sport";
    }
}

internal class City: Car
{
    LicenseService m_ls;
    internal City(LicenseService ls)
    {
        this.m_ls = ls;
        this.BuyAccessor += ls.GenerateLicense;
        SetName();
    }
    internal override void SetName()
    {
        this.CarName = "City";
    }
}

LicenseService

internal class LicenseService
{
    internal void GenerateLicense(Car sender, Random rnd)
    {
        string carName = sender.CarName;
        string licenseNumber = "";
        for(int i = 0; i < 5; i++)
        {
            licenseNumber += rnd.Next(0, 9).ToString();
        }
        Console.WriteLine("{1} Car has been bought, this is the license number: {0}", licenseNumber, carName);
    } 
}

並呼吁流動

static void Main(string[] args)
{
    Random rnd = new Random();
    LicenseService ls = new LicenseService();
    Sport sport = new Sport(ls);
    City city = new City(ls);

    city.OnBuy(rnd);
    sport.OnBuy(rnd);

    Console.ReadLine();
}

這是一個很長的答案,但我會首先解釋你的例子,然后繼續一些一般性的提示。 請記住,所有代碼都是偽代碼,如果沒有語法調整,它將無法編譯。

首先,你的邏輯結構沒有意義,這就是為什么你可能很難確定這是否正確。

例如,在現實世界中,您不會在汽車上購買它,而是在商店或銷售它們的服務中找到它們。 您只能駕駛汽車或使用其提供的其他功能。 汽車沒有為自己分配許可證。 最后,如果您采用基本的賣家/買家示例,則購買是一個通常是線性的(並且可以通過方法表示,沒有觸發器)。 因此,當您調用shop.BuyCar( sportsCar ) ,可以從buy方法調用所有購買邏輯。

Class Shop{ 

    public Car BuyCar( carWithoutLicense ){
        //Purchase logic
        LicenseService.AssignLicense( carWithoutLicense ).
        return carWithoutLicense.
    }
}
//A person would call this method, no the car

正確使用事件的一個更好的例子是汽車前面板上的警示燈之一,因為它可以通知駕駛員他/她可能想要做出的反應。 例如:檢查發動機燈。

class Car {
   Bool CheckEngingLightIsOn = false;
   public void Ride( ... ){
    if( enginge.faultDetected == true ){
       TurnOnCheckEngineAlert( );
    }
   }
   public void TurnOnCheckEngineAlert( ){
      CheckEngingLightIsOn = true;
      if( CheckEngineLightSwitch != null ){
         CheckEngineLightSwitch.Invoke( ... )
      }
   }
}

class Driver {
   public Driver( Car car ){
      this.car = car;
      if( driverState != Drunk ){
       car.CheckEngineLightSwitch = TakeAction;
      }
   }
   public Drive( ){
      car.Ride( );
   }
   public void TakeAction( Car car, EventArgs e ){
      //get out, open the hood, check the engine...
      if( car.CheckEngingLightIsOn == true ){ "Light turned on
        //Check Engine
      }else{
        //continue driving
      }
   }
}

如果不深入抽象,請注意事件鏈:

  • Driver駕駛汽車並且在發生之前不擔心其他事情(例如檢查發動機燈)。
  • 如果Car檢測到故障,它會打開檢查引擎燈,並且該事件(訂戶)上有事件處理程序,汽車會觸發事件。
  • 事件觸發,但驅動程序必須訂閱它才能注意到更改。
  • 只有當駕駛員訂閱了該事件時(在這種情況下,如果沒有喝醉),他將根據該事件采取行動。

此示例與您的示例根本不同,因為:

  • 駕駛汽車時,駕駛員無需始終注意檢查發動機燈(即使他可以檢查)。
  • 這是檢查發動機狀態並在發動機燈上表示它的汽車的標准過程。
  • 驅動程序和汽車都會影響彼此的進一步行動,如果線性表達,這種邏輯效率低下。

換句話說,購買過程是固定的(支付,許可,貨物轉移)和跳過必不可少的步驟不能跳過。 汽車自行移動的過程並非一成不變,因為汽車和司機都不知道旅程中會發生什么。 IE驅動程序可能會在沒有停止的情況下開車到達目的地(如果沒有出現故障或者他沒有注意到燈光),或者當車輛注意到檢查發動機指示燈亮起時汽車可能會強迫駕駛員停車(基本上,他們都在相互控制感覺)。


關於您的用例的一些常規提示

在你的例子中你做了一個有點復雜的用例(錯誤放置邏輯),它將運行,但結構上是不正確的(在設計更多邏輯時,錯誤的邏輯結構往往會導致人為錯誤)。

您應該首先看到的是與每個對象相關的邏輯。 對象中的事件/方法是代表對象所做的事情(即對象自身執行的功能)還是影響對象的東西,但對象本身在進程中沒有做任何事情? 例如:汽車自行“騎”(即使這個過程開始,其所有參數,如速度或方向都由駕駛員控制); 為汽車分配許可證完全發生在汽車結構之外,並且汽車只會在過程中獲得屬性更改 (許可證)。

這種區別很重要,因為只有對象執行的邏輯才與該對象相關,並且通過擴展,任何由另一個對象執行並且僅影響您的對象的邏輯都屬於其他地方。 因此, Buy絕對不屬於汽車而且Ride(移動過程)屬於汽車。

其次,您的命名將有助於您理解此主題。 方法代表的行動和應該被命名為這樣( Shop.BuyCarCar.RideDriver.Drive ),事件代表反應觸發器Car.CheckEngineLightSwitch )和事件處理程序表示反應的動作(反應仍然是一個動作,所以沒有需要特殊命名,但您可以命名為區分動作和反應)。

不。

除了擁有該事件的類之外,任何人都不應該調用事件(或者,如果您知道自己在做什么,那么繼承自擁有該事件的類的類 - 廣告即使這樣,他們也應該注意原始事件實施以避免細微問題)

基本上,對事件的訂閱是一種承諾,當給定的事情發生時,您將調用您從訂閱者那里獲得的功能。 事件本身很簡單,代碼構造允許您進行訂閱,而無需知道或實現多播函數指針調用的復雜性。

否則,你只是在調用一個函數,也可能不會對事件感到煩惱。

事件本質上是有意的代碼注入 - 它們允許你讓一些其他類執行你編寫的任意代碼,當他們做某事時。

我建議你向汽車注入許可證服務,並在購買時調用生成許可證功能,

using System;

namespace ConsoleApp1.Test
{

    class Program
    {
        static void Maintest(string[] args)
        {
            ILicenseService licenseService = new LicenseService();

            Sport sport = new Sport(licenseService);
            City city = new City(licenseService);

            //car.Name();
            //sport.Name();
            //city.Name();

            //Console.ReadLine();            

            city.OnBuy();

            Console.ReadLine();
        }
    }

    internal abstract class Car
    {
        protected readonly ILicenseService licenseService;

        public Car(ILicenseService _licenseService)
        {
            licenseService = _licenseService;
        }

        internal virtual void Name()
        {
            Console.WriteLine("Car");
        }

        internal event EventHandler Buy;

        internal virtual void OnBuy()
        {
            // TODO
        }
    }

    internal class Sport : Car
    {
        public Sport(ILicenseService _licenseService) : base(_licenseService)
        {

        }

        internal override void Name()
        {
            Console.WriteLine("Sport");
        }

        internal override void OnBuy()
        {
            licenseService.GenerateLicense(new object());
        }
    }

    internal class City : Car
    {
        public City(ILicenseService _licenseService) : base(_licenseService)
        {

        }

        internal override void Name()
        {
            Console.WriteLine("City");
        }

        internal override void OnBuy()
        {
            licenseService.GenerateLicense(new object());
        }
    }

    internal interface ILicenseService
    {
        void GenerateLicense(object param);
    }

    internal class LicenseService : ILicenseService
    {
        public void GenerateLicense(object param)
        {
            // do your stuff
        }
    }

}

1)這是在C#上使用Event的正確方法嗎?

不完全的。 OnBuy應該是受保護的虛擬方法。 這也排除了你從Main()方法調用它。

更常見的是調用someCar.Buy()然后Buy()將觸發OnBuy()。

2)如果這是正確的。 我可以將OnBuy方法覆蓋到每個孩子身上嗎? 如果覆蓋可用,我該怎么辦?

是的,你可以覆蓋它看到這是一種更有效的訂閱自己的方式(這將是另一種選擇)。

購買特定類型的汽車時,您可以執行任何操作。 但是做alwas call base.OnBuy()

3)從這個樣本中我可以做些什么來改善它?

CreateLicense聽起來不像是一個事件的好候選者,它更像是CarDealer調用的業務規則。

遵循現代設計規則,汽車將是一個相當被動的實體對象(貧血領域模型)。

事件通常用於告訴其他組件“我已經改變,做你的事”,而不是對自己執行必要的操作。

以下是我的首選設計:

class Program
{
    static void Main(string[] args)
    {
        Car car = new Sport();
        car.BuyEvent += License.GenerateLicense;        
        car.OnBuy();
        car = new City();
        car.BuyEvent += License.GenerateLicense;
        car.OnBuy();
    }
}

internal abstract class Car
{
    internal abstract void Name();

    protected abstract event EventHandler Buy;

    public abstract event EventHandler BuyEvent;

    public abstract void OnBuy();   
}

internal class Sport : Car
{
    internal override void Name()
    {
        Console.WriteLine("Sport Car");
    }

    protected override event EventHandler Buy;

    public override event EventHandler BuyEvent
    {
        add
        {
            lock (this)
            {
                Buy += value;
            }
        }

        remove
        {
            lock (this)
            {
                Buy -= value;
            }
        }
    }

    public override void OnBuy()
    {
        if (Buy != null)
        {
            Buy(this, EventArgs.Empty);
        }
    }

}

internal class City : Car
{
    internal override void Name()
    {
        Console.WriteLine("City Car");
    }

    protected override event EventHandler Buy;

    public override event EventHandler BuyEvent
    {
        add
        {
            lock (this)
            {
                Buy += value;
            }
        }

        remove
        {
            lock (this)
            {
                Buy -= value;
            }
        }
    }

    public override void OnBuy()
    {
        if (Buy != null)
        {
            Buy(this, EventArgs.Empty);
        }
    }
}

internal static class License
{

    public static void GenerateLicense(object sender, EventArgs args)
    {
        Random rnd = new Random();

        string carType = sender.GetType().Name;

        string licenseNumber = "";

        for (int i = 0; i < 5; i++)
        {
            licenseNumber += rnd.Next(0, 9).ToString();
        }

        Console.WriteLine($"{carType} Car has been bought, this is the license number: {licenseNumber}");
    }

}

重點:

  1. 使Car基類抽象,它只能像City / Sport Car一樣派生和使用
  2. 在每個子類中添加event add / remove accessor以自定義事件處理。 使訪問者線程對共享客戶端訪問安全
  3. Make License - GenerateLicense是靜態的,因為類中沒有狀態/數據,或者通過另一個基本的抽象方法將它們與City / Sport class實現集成在一起
  4. 抽象類Car應在運行時注入Sport \\ City對象,並應通過基類Car對象用於事件訂閱

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM