C#事件的实现

时间:2018-05-17 06:04:12

标签: c# class events

我正在学习关于类的C#事件实现。

我有例子:

Sport类和City类继承自Car类。 Car class具有由Sport和City类继承的基本方法调用OnBuy。在OnBuy方法中,事件处理程序已分配给Event Buy。

还有一个名为LicenseService的服务或类,每次购买时都会生成许可证号。

在这种情况下我实现了事件驱动编程。 这是我的git示例:

https://github.com/adityosnrost/CSharpLearningEvent

问题:

  1. 这是在C#上使用Event的正确方法吗?

  2. 如果这是正确的。我可以将OnBuy方法覆盖到每个孩子身上吗?如果覆盖可用,我该怎么办?

  3. 从这个示例中我可以做些什么来改善它?

  4. 谢谢

    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);
        } 
    }
    

6 个答案:

答案 0 :(得分:5)

如果我正在制作这个节目,我会做出以下更改,

  • 活动签名

首先它有两个参数ObjectEventArgs,您只需要处理程序方法中的Car(以及下面讨论的Random)。

  • 我会在Child的构造函数中传递LicenseService,并仅在构造函数中注册(subscribe)事件。那将是更清洁的方式。
  • 会在父类中创建一个字符串成员CarName,这样每个孩子都可以在任何他们想要的地方使用它。
  • 我还没有在此代码中做过的另一件事,我永远不会将事件命名为Buy,而是将其命名为Bought
  • (这仅适用于此方案)在您的代码中,GenerateLicense()内每次都会创建Random的新对象。因此,如果您对该方法的两次调用都在很短的时间内,它将为两个调用生成相同的随机数。为什么?看到这个Question - 或者您可以自己尝试下面的示例代码。所以我会在Random中传递已经创建的GenerateLicense()对象。因此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并且所有订阅都消失了。它需要一个事件访问器,因此可以使用+=-=运算符访问事件,并且您自己可以为多个订户设置线程安全,否则会出现竞争条件,请检查{{3} }

以下是代码,

你的班级结构:

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();
}

答案 1 :(得分:3)

这是一个很长的答案,但我会首先解释你的例子,然后继续一些一般性的提示。请记住,所有代码都是伪代码,如果没有语法调整,它将无法编译。

首先,您的逻辑结构没有意义,这就是为什么您可能难以确定这是否正确。

例如在现实世界中,您不会在汽车上购买它,而是在商店或销售它们的服务中找到它们。您只需要驾驶汽车,或使用其提供的其他功能 。汽车没有为自己分配许可证。最后,如果您采用基本的卖家/买家示例,则购买是一个通常是线性的(并且可以通过方法表示,没有触发器)。因此,当您致电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检测到故障,它会打开检查引擎灯,并且该事件(订户)上有事件处理程序,汽车会触发事件。
  • 事件触发,但必须订阅驱动程序以注意更改。
  • 只有当司机订阅了该事件时(在这种情况下,如果没有喝醉),他将根据该事件采取行动。

此示例与您的示例根本不同,因为:

  • 驾驶汽车时,驾驶员无需始终注意检查发动机灯(即使他可以检查)。
  • 检查发动机状态并在发动机灯上代表它是汽车的标准过程。
  • 驱动程序和汽车都会影响彼此的进一步行动,如果线性表达,这种逻辑效率很低。

换句话说,购买流程是一成不变的(付款,许可,货物转移),跳过必不可少的步骤是不能跳过的。汽车自行移动的过程并非一成不变,因为汽车和司机都不知道旅程中会发生什么。 I.E.驾驶员可以在不停车的情况下驾驶到目的地(如果没有发生故障或者他没有注意到灯光),或者当驾驶员注意到检查发动机指示灯亮起时,汽车可能会强迫驾驶员停下来(基本上,他们都会在义)。

有关您的用例的一些常规提示

在你的例子中你做了一个有点复杂的用例(错误放置的逻辑),它将运行,但结构上是不正确的(在设计更多逻辑时,错误的逻辑结构往往会导致人为错误)。

首先要注意的是与每个对象相关的逻辑。对象中的事件/方法是代表对象执行的操作(即对象自身执行的功能)还是影响对象但是对象本身不执行的操作什么在进行中?例如:汽车自行“骑”(即使这个过程开始,其所有参数,如速度或方向都由驾驶员控制);为汽车分配许可证完全发生在汽车结构之外且汽车仅获得属性更改(许可证)正在进行中。

这种区别很重要,因为只有您的对象执行的逻辑与该对象相关,并且通过扩展,任何由另一个对象执行并且仅影响您的对象的逻辑无关紧要属于其他地方。所以Buy绝对不属于汽车而且Ride(移动过程)属于汽车。

其次,您的命名将有助于您理解此主题。方法代表操作,并且应该这样命名(Shop.BuyCarCar.RideDriver.Drive),事件代表反应触发器({{ 1}})和事件处理程序将反应表示为一个动作(反应仍然是一个动作,因此不需要特殊的命名,但你可以命名为区分动作和反应)

答案 2 :(得分:2)

以下是我的首选设计:

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. License - GenerateLicense设为静态,因为类中没有状态/数据,或者通过另一个基本抽象方法将它们与City / Sport class实现集成在一起
  4. 抽象类Car应在运行时注入Sport \ City个对象,并应通过基类Car对象用于事件订阅

答案 3 :(得分:1)

不。

除了拥有该事件的类之外的任何人都不应该调用事件(或者,如果您知道自己正在做什么,那么从拥有该事件的类继承的类 - 即使这样,它们也应该是注意原来的实施,以避免微妙的问题)

基本上,对事件的订阅是一种承诺,当给定的事情发生时,您将调用您从订阅者那里获得的功能。事件本身很简单,代码构造允许您进行订阅,而无需知道或实现多播函数指针调用的复杂性。

否则,您只是在调用一个函数,并且可能不会对事件感到烦恼。

事件本质上是有意的代码注入 - 它们允许你让一些其他类执行你编写的任意代码,当他们做某事时。

答案 4 :(得分:1)

我建议您向汽车注入许可服务,并在调用购买时调用生成许可功能

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
        }
    }

}

答案 5 :(得分:1)

  

1)这是在C#上使用Event的正确方法吗?

不完全。 OnBuy应该是受保护的虚拟方法。这也排除了你从Main()方法调用它。

更常见的是调用someCar.Buy()然后Buy()将触发OnBuy()。

  

2)如果这是正确的。我可以将OnBuy方法覆盖到每个孩子身上吗?如果覆盖可用,我该怎么办?

是的,你可以覆盖它看到一个更有效的订阅自己的方式(这将是另一种选择)。

购买特定类型的汽车时,您可以执行任何操作。但是请致电base.OnBuy()

  

3)从这个样本中我可以做些什么来改善它?

CreateLicense听起来并不像是一个好事件的候选者,它更像是CarDealer调用的商业规则。

遵循现代设计规则,Car将是一个相当被动的实体对象(Anemic Domain Model)。

事件通常会被用来告诉其他组件&#34;我已经改变了,做了你的事情&#34;,而不是对自己进行必要的行动。