【问题标题】:Why sending events for a sender is forbidden in C#?为什么在 C# 中禁止为发送者发送事件?
【发布时间】:2010-09-04 15:12:58
【问题描述】:

引用自: http://msdn.microsoft.com/en-us/library/aa645739(VS.71).aspx

“只能在声明事件的类中调用事件。”

我很困惑为什么会有这样的限制。如果没有这个限制,我将能够编写一个类(一个类),它曾经可以很好地管理发送给定类别的事件——比如 INotifyPropertyChanged

由于这个限制,我必须重新复制和粘贴相同(相同!)的代码。我知道 C# 的设计者不太重视代码重用 (*),但是,哎呀……复制和粘贴。效率如何?

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string name)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }

在每一堂课中都会改变一些东西,直到你生命的尽头。可怕!

所以,当我将额外的发送类(我太轻信)恢复为旧的“好”复制和粘贴方式时,你能看到

为发送者发送事件的能力会发生什么可怕的事情?

如果您知道如何避免此限制的任何技巧 - 也不要犹豫回答!

(*) 多继承我可以用更清晰的方式编写一次通用发送方,但是 C# 没有多继承

编辑

迄今为止最好的解决方法

介绍界面

public interface INotifierPropertyChanged : INotifyPropertyChanged
{
    void OnPropertyChanged(string property_name);
}

为 PropertyChangedEventHandler 添加新的扩展方法 Raise。然后为这个新接口添加中介类,而不是基本的 INotifyPropertyChanged。

到目前为止,让我们代表其所有者从嵌套对象向您发送消息的代码最少(当所有者需要此类逻辑时)。

感谢大家的帮助和想法。

编辑 1

古法写道:

“你不能通过从外部触发事件来导致某事发生,”

这很有趣,因为...我可以。这正是我要问的原因。看看吧。

假设你有类字符串。不有趣,对吧?但是让我们将它与 Invoker 类一起打包,它会在每次更改时发送事件。

现在:

class MyClass : INotifyPropertyChanged
{
    public SuperString text { get; set; }
}

现在,当文本改变时,MyClass 也改变了。因此,当我在文本中时,我知道,如果只有我拥有所有者,它也会被更改。所以我可以代表它发送事件。而且它在语义上是 100% 正确的。

备注:我的班级只是稍微聪明一点——所有者设置它是否希望有这样的逻辑。

编辑 2

传递事件处理程序的想法——“2”不会显示。

public class Mediator
{
    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(string property_name)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(property_name));
    }

    public void Link(PropertyChangedEventHandler send_through)
    {
        PropertyChanged += new PropertyChangedEventHandler((obj, args) => {
            if (send_through != null)
                send_through(obj, args);
        });
    }

    public void Trigger()
    {
        OnPropertyChanged("hello world");
    }
}
public class Sender
{
    public event PropertyChangedEventHandler PropertyChanged;

    public Sender(Mediator mediator)
    {
        PropertyChanged += Listener1;
        mediator.Link(PropertyChanged);
        PropertyChanged += Listener2;

    }
    public void Listener1(object obj, PropertyChangedEventArgs args)
    {
        Console.WriteLine("1");
    }
    public void Listener2(object obj, PropertyChangedEventArgs args)
    {
        Console.WriteLine("2");
    }
}

    static void Main(string[] args)
    {
        var mediator = new Mediator();
        var sender = new Sender(mediator);
        mediator.Trigger();

        Console.WriteLine("EOT");
        Console.ReadLine();
    }

编辑 3

作为对所有关于滥用直接事件调用的争论的评论——滥用当然仍然是可能的。只需实施上述解决方法即可。

编辑 4

我的代码小示例(最终使用),Dan请看一下:

public class ExperimentManager : INotifierPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged(string property_name)
    {
        PropertyChanged.Raise(this, property_name);
    }


    public enum Properties
    {
        NetworkFileName,
        ...
    }

    public NotifierChangedManager<string> NetworkFileNameNotifier;
    ...

    public string NetworkFileName 
    { 
         get { return NetworkFileNameNotifier.Value; } 
         set { NetworkFileNameNotifier.Value = value; } 
    }

    public ExperimentManager()
    {
        NetworkFileNameNotifier = 
            NotifierChangedManager<string>.CreateAs(this, Properties.NetworkFileName.ToString());
        ... 
    }

【问题讨论】:

  • 一直在想同样的问题很多次 :) 等待一个好的答案。
  • (*) 不是C#不支持多重继承,是.NET框架。
  • 有一个东西是 too 太多抽象,将这 2 行(或在您的情况下为 4 行)代码的工作委托给外部帮助程序类似乎很棒例子。
  • 另外,您可以在 PropertyChangedEventHandler 上创建一个名为“Raise”的扩展方法,以便您的实现是 PropertyChanged.Raise(this, name);对我来说似乎很简洁。
  • @macias:我认为你误解了 Kirk Woll 的建议。在PropertyChangedEventHandler 上编写一个扩展方法,完全可以做到他所描述的。然后,您将在声明事件的类中使用此扩展方法,而无需编写您的OnPropertyChanged 代码。它不会为您节省所有的输入;但它确实可以为您节省一些。

标签: c# events


【解决方案1】:

在开始咆哮之前考虑一下。如果任何方法都可以在任何对象上调用事件,那会不会破坏封装并且也会造成混淆?事件的意义在于,具有事件的类的实例可以通知其他对象某些事件已发生。事件必须来自该类而不是任何其他类。否则,事件将变得毫无意义,因为任何人都可以随时触发任何对象上的任何事件,这意味着当事件触发时,您无法确定它是否真的是因为它所代表的动作发生了,或者只是因为某些第 3 方类决定玩得开心。

也就是说,如果您希望能够允许某种中介类为您发送事件,只需使用添加和删除处理程序打开事件声明即可。然后你可以这样做:

public event PropertyChangedEventHandler PropertyChanged {
    add {
        propertyChangedHelper.PropertyChanged += value;
    }
    remove {
        propertyChangedHelper.PropertyChanged -= value;
    }
}

然后 propertyChangedHelper 变量可以是某种对象,它实际上会为外部类触发事件。是的,您仍然需要编写添加和删除处理程序,但它相当少,然后您可以使用任何您想要的复杂性的共享实现。

【讨论】:

  • 如果你一直使用它会很混乱——但关键是没有人强迫你使用它(与当前状态完全相反)。它类似于 Java 中的运算符重载——它是(或曾经?)被禁止的,因为某个地方的某些人可能会以一种糟糕的方式重载它。出于这个原因,所有开发人员都不允许这样做。我在这里不太了解中介模式——我已经有一个从 INotifyPropertyChanged 继承的类(而且必须是这样),中介(我在发布问题之前写的)无法调用事件这个类,因为它是外部的。
  • 我不认为它等同于那个。一个类从来没有一个很好的理由来调用另一个对象的事件。那是从一个事件的意义。换句话说,我认为这不可能正确地完成,至少从语义的角度来看是这样。至少,通过运算符重载,它在许多情况下实际上是有意义的。
  • 我的术语很草率。它不一定是中介。关键是您有一些实现属性更改通知业务的类。这是作为多重继承的替代方案(使用组合代替)。
  • 是的,这就是我的情况——所以它是正确的(就工作流逻辑而言),但我无法对其建模。
  • 不正确。从另一个类发送事件永远不会正确。如果您的工作流程逻辑需要这样做,那么您需要重新考虑您的工作流程或实施,或两者兼而有之。
【解决方案2】:

允许任何人提出任何事件会使我们陷入这个问题:

@Rex M:大家好,@macias 刚刚举手了!

@macias:不,我没有。

@大家:太晚了! @Rex M 说你做到了,我们都相信了这一点。

此模型旨在防止您编写容易出现无效状态的应用程序,这是最常见的错误来源之一。

【讨论】:

  • 即使您将发送事件更改,并不意味着状态无效。我试图在这里权衡外部调用的成本,甚至是内部调用的成本。允许任何人提出任何事件并不意味着你会在所有课程中都写这个,对吧?但是强迫程序员复制粘贴代码意味着你必须在你所有的类中复制粘贴它。
  • @macias 这并不意味着状态无效,它只是打开了大门,让您在状态无效的情况下编写代码变得更加容易,交换的好处很少。
  • 对我的好处是显而易见的——它大大节省了我的时间。是时候复制和粘贴了,是时候重新分析相同的代码了。重用概念并不是无缘无故发明的——这对我来说是一个很大的节省。
  • @macias 听起来像-正如您在问题末尾所说的那样-多重继承可能是解决此问题的好方法。但是,不允许外部调用者引发事件。
  • 是的,这样会好很多——因为在这种情况下,发送者会发送自己的消息。
【解决方案3】:

我认为您误解了限制。这是要说的是,只有声明事件的类才能真正引发它。这与编写静态帮助器类来封装实际的事件处理程序实现不同。

声明事件B 的类外部的类A 不应导致B 直接引发该事件。 A 必须引起 B 引发事件的唯一方法是对 B 执行一些操作,执行该操作会引发事件。

对于INotifyPropertyChanged,给定以下类:

public class Test : INotifyPropertyChanged
{
   public event PropertyChangedEventHandler PropertyChanged;

   private string name;

   public string Name
   {
      get { return this.name; }
      set { this.name = value; OnNotifyPropertyChanged("Name"); }
   }

   protected virtual void OnPropertyChanged(string name)
   {
       PropertyChangedEventHandler  temp = PropertyChanged;
       if (temp!= null)
       {
          temp(this, new PropertyChangedEventArgs(name));
       }
   }
}

使用Test 的类导致Test 引发PropertyChanged 事件的唯一方法是设置Name 属性:

public void TestMethod()
{
    Test t = new Test();
    t.Name = "Hello"; // This causes Test to raise the PropertyChanged event
}

你不会想要这样的代码:

public void TestMethod()
{
    Test t = new Test();
    t.Name = "Hello";
    t.OnPropertyChanged("Name");
}

话虽如此,编写一个封装实际事件处理程序实现的帮助类是完全可以接受的。例如,给定以下EventManager 类:

/// <summary>
/// Provides static methods for event handling. 
/// </summary>
public static class EventManager
{
    /// <summary>
    /// Raises the event specified by <paramref name="handler"/>.
    /// </summary>
    /// <typeparam name="TEventArgs">
    /// The type of the <see cref="EventArgs"/>
    /// </typeparam>
    /// <param name="sender">
    /// The source of the event.
    /// </param>
    /// <param name="handler">
    /// The <see cref="EventHandler{TEventArgs}"/> which 
    /// should be called.
    /// </param>
    /// <param name="e">
    /// An <see cref="EventArgs"/> that contains the event data.
    /// </param>
    public static void OnEvent<TEventArgs>(object sender, EventHandler<TEventArgs> handler, TEventArgs e)
         where TEventArgs : EventArgs
    {
        // Make a temporary copy of the event to avoid possibility of
        // a race condition if the last subscriber unsubscribes
        // immediately after the null check and before the event is raised.
        EventHandler<TEventArgs> tempHandler = handler;

        // Event will be null if there are no subscribers
        if (tempHandler != null)
        {
            tempHandler(sender, e);
        }
    }

    /// <summary>
    /// Raises the event specified by <paramref name="handler"/>.
    /// </summary>
    /// <param name="sender">
    /// The source of the event.
    /// </param>
    /// <param name="handler">
    /// The <see cref="EventHandler"/> which should be called.
    /// </param>
    public static void OnEvent(object sender, EventHandler handler)
    {
        OnEvent(sender, handler, EventArgs.Empty);
    }

    /// <summary>
    /// Raises the event specified by <paramref name="handler"/>.
    /// </summary>
    /// <param name="sender">
    /// The source of the event.
    /// </param>
    /// <param name="handler">
    /// The <see cref="EventHandler"/> which should be called.
    /// </param>
    /// <param name="e">
    /// An <see cref="EventArgs"/> that contains the event data.
    /// </param>
    public static void OnEvent(object sender, EventHandler handler, EventArgs e)
    {
        // Make a temporary copy of the event to avoid possibility of
        // a race condition if the last subscriber unsubscribes
        // immediately after the null check and before the event is raised.
        EventHandler tempHandler = handler;

        // Event will be null if there are no subscribers
        if (tempHandler != null)
        {
            tempHandler(sender, e);
        }
    }
}

如下更改Test 是完全合法的:

public class Test : INotifyPropertyChanged
{
   public event PropertyChangedEventHandler PropertyChanged;

   private string name;

   public string Name
   {
      get { return this.name; }
      set { this.name = value; OnNotifyPropertyChanged("Name"); }
   }

   protected virtual void OnPropertyChanged(string name)
   {
       EventHamanger.OnEvent(this, PropertyChanged, new PropertyChangedEventArgs(name));
   }
}

【讨论】:

  • 您知道,在OnEvent 重载中使用那些tempHandler 局部变量是多余的。不存在将本地复制的变量重新分配到堆栈的竞争条件!
  • 感谢您的代码。但是,您的 Rest 类中仍然有此方法 OnPropertyChanged。查看这一行:“this.name = value”。名字改了吗?是的。所以在这一点上你知道测试改变了。这个想法是通过“测试”将事件从“名称”发送到它的侦听器。
  • @macias:你刚才描述的行为实际上不仅不允许,而且完全不可能,因为name 变量name 指向的object 不同。也就是说,对象对变量一无所知(它怎么可能?)——见我的更新。 (尽管目前不合法,但仍有一些 like 您所要求的内容可能理论上 是可能的。这就是我在回答中所说的。)
  • @Dan,好吧,这取决于您使用什么类作为名称。在我的情况下,它是内置通知管理器的字符串。
  • @Dan Tao:其实一点也不多余。关键是,如果他不使用局部变量,而只是在调用该字段之前对其进行了测试,则会出现竞争条件(在测试该字段然后调用它之间)。通过将字段复制到局部变量,字段是否更改将不再重要,因为您可以使用本地副本,您正确指出,不能更改。
【解决方案4】:

首先我必须说,事件实际上意味着只有在任何外部事件处理程序附加到它时才从对象内部调用。

所以基本上,事件会为您提供来自对象的回调,并让您有机会为其设置处理程序,以便在事件发生时自动调用该方法。

这类似于向成员发送变量值。您还可以定义一个委托并以相同的方式发送处理程序。所以基本上它是分配函数体的委托,最终当类调用事件时,它将调用方法。

如果你不想在每个类上都做这样的事情,你可以轻松地创建一个 EventInvoker 来定义它们中的每一个,并在它的构造函数中传递委托。

public class EventInvoker
{
   public EventInvoker(EventHandler<EventArgs> eventargs)
   {
      //set the delegate. 
   } 

   public void InvokeEvent()
   {
        // Invoke the event. 
   }
}

所以基本上你在这些方法中的每一个上创建一个代理类,并使这个泛型可以让你为任何事件调用事件。这样您就可以轻松避免每次都调用这些属性。

【讨论】:

  • 谢谢,但是我可以用这个 Invoker 类做什么?正如我提到的 C# 没有多重继承,所以在 99% 的情况下我不能从它继承(就像 WPF 中的 MyWindow 已经从 Window 继承),没有它我无法从 Invoker 调用事件。
  • 不,您不继承它,而是创建类的对象并使用事件访问器将委托传递给该类。
  • 如果我没看错的话,它是行不通的。当您传递侦听器列表时,您得到的只是传递时的侦听器——以后的任何更改都将不可见。见编辑2。
  • 构建一个观察者怎么样。就像使用 Reactive Framework 一样。
【解决方案5】:

事件旨在通知发生了某事。声明事件的类负责在正确的时间触发事件。

你不能通过从外部触发事件来导致某事发生,你只会让每个事件订阅者认为它已经发生了。让事情发生的正确方法是让事情发生,而不是让它看起来像发生了。

因此,允许从类外部触发事件几乎只能被误用。如果出于某种原因从外部触发事件很有用,则该类可以轻松地提供允许它的方法。

【讨论】:

  • 我通过从外部触发事件来导致某些事情发生。查看修改。
  • @macias:不,触发 PropertyChanged 事件不会导致属性更改。您只是试图将触发事件的责任移出类,这只会使代码更难遵循。
  • 假设您创建了一个Button 类,当有人点击它时会引发Presssed 事件。为什么能够创建Button 的子类在有人按下Enter 键时引发Pressed 事件是不好的?
  • @Gabe:没关系,因为课程是Button。然后你可以按照框架中的做法,创建一个引发事件的方法protected virtual void OnPressed(EventArgs e)
  • Guffa:但是 OnPressed 函数只是必要的,因为子类不能从其基类引发事件。我认为OnXXX 约定的存在证明了从其类之外引发事件有很好的用途。
【解决方案6】:

更新

好的,在我们继续之前,我们肯定需要澄清一点。

你似乎希望这种情况发生:

class TypeWithEvents
{
    public event PropertyChangedEventHandler PropertyChanged;

    // You want the set method to automatically
    // raise the PropertyChanged event via some
    // special code in the EventRaisingType class?
    public EventRaisingType Property { get; set; }
}

您真的希望能够像这样编写代码吗?这真的是完全 不可能——至少在 .NET 中是这样(至少在 C# 团队专门为 INotifyPropertyChanged 接口提出一些花哨的新语法糖之前,我相信实际上已经讨论过了)——作为一个 object 没有“分配给我的变量”的概念。事实上,根本没有办法使用对象来表示 变量(我想 LINQ 表达式实际上是一种方式,但那是完全不同的主题)。它只能以相反的方式工作。所以假设你有:

Person x = new Person("Bob");
x = new Person("Sam");

“Bob”知道x 刚刚被分配给“Sam”吗? 绝对不是:变量x 只是指向“Bob”,它从来没有“Bob”;所以“鲍勃”根本不知道也不关心x 会发生什么。

因此,对象不可能希望根据指向它的变量何时更改为指向其他对象来执行某些操作。就好像你在信封上写了我的名字和地址,然后你把它擦掉并写了别人的名字,我不知何故神奇地知道,@macias 只是把信封上的地址从我的地址改成了别人的!

当然,您可以做的是修改一个属性,以便它的getset 方法修改一个私有成员的不同属性,并将您的事件链接到该成员提供的事件(这本质上是siride has suggested)。在这种情况下,某种需要您所询问的功能是合理的。这是我在最初的答案中想到的场景。


原答案

我不会说您所要求的完全是错误的,正如其他一些人似乎暗示的那样。显然可能允许类的私有成员引发该类的事件之一,例如在您描述的场景中。虽然saurabh's idea 是一个不错的选择,但很明显,由于 C# 缺少多重继承*,它并不总是适用。

这让我明白了。为什么 C# 允许多重继承?我知道这似乎离题,但这个问题和那个问题的答案是一样的。并不是说它是非法的,因为它“永远不会”有意义;这是非法的,因为利弊多于利弊。 多重继承很难做到正确。同样,您所描述的行为也很容易被滥用。

也就是说,是的,the general case Rex has described 提出了一个很好的论据来反对引发其他对象事件的对象。另一方面,您所描述的场景——样板代码的不断重复——似乎使某种情况支持这种行为。问题是:应该给予哪个考虑更大的权重?

假设 .NET 设计人员决定允许这样做,只是希望开发人员不要滥用它。几乎可以肯定会有很多更多损坏的代码,其中X 类的设计者没有预料到E 事件会被Y 类引发,而在另一个程序集中。但它确实如此,并且X 对象的状态变得无效,并且到处都有细微的错误。

相反的情况呢?如果他们不允许呢?当然,现在我们只是考虑现实,因为 的情况。但是这里的巨大缺点是什么?您必须在很多地方复制并粘贴相同的代码。是的,这很烦人;但也有一些方法可以缓解这种情况(例如 saurabh 的基类思想)。事件的引发是由声明类型always严格定义的,这让我们对程序的行为有了更大的确定性。

所以:

活动政策 |优点 |缺点 ------------------------------+------------------ --+------------- 允许任何物体升起 |少输入 |少得多的控制 另一个对象的事件 |某些情况|阶级行为,丰富 | |意想不到的舞蹈 | |情景,扩散 | |微妙的错误 | | -------------------------------------------------- ---------------------------- 将事件限制为仅 |更好的控制 |需要更多打字 由声明类型引发 |类行为,|在某些情况下 |没有意外 | |情景,意义- | |不能减少| |错误计数 |

假设您负责决定为 .NET 实施哪个事件策略。你会选择哪一个?

*我说“C#”而不是“.NET”,因为我实际上不确定禁止多重继承是 CLR 的事情,还是只是 C# 的事情。有人碰巧知道吗?

【讨论】:

  • 你的桌子唯一的问题是(谢谢)你把人混在一起了。是的。我受到惩罚是因为 Joe Doe 不知道如何编写程序。我强烈支持“一切都应该是可行的,容易的事情容易,奇怪的事情 - 困难”的想法。提前禁止某事是虚荣心的轻微表现,因为从时间的角度来看,这意味着“我们预测了每一种可能的用途”。我不相信如此巨大的期待。关于这个问题——我没有使用很多危险的特性(Perl 中的 $_,任何人),我也不会为此上瘾 :-)
  • @macias:首先,“混人”是不可避免的,不是吗?每个开发人员都不能拥有自己的编程语言和自己的一套规则。其次,您对编程语言的理念与 C# 理念不同。问题再次归结为利弊。通过广泛访问和功能丰富,同时以旨在保护普通(有时粗心)开发人员的方式限制使用,C# 已成为一种非常流行和有用的语言。他们本可以让它变得不那么死板,并且会编写更多错误的程序。
  • 人气与质量无关。第二,当您查看 C# 框架时,您会注意到尽管大肆宣传它是静态类型的,但它实际上是非常动态的语言,神奇的文字一直在飞来飞去(例如在 XAML 中)。但是触发更改事件也是一个很好的例子。没有 nameof(MyProperty) 只有“MyProperty”(是的,我知道解决方法),所以恕我直言,C# 远非可靠的防弹语言。
  • 现在,关于更新——更新的最后一段就是我的情况。也许我添加它只是为了记录。请参阅编辑 4——我希望我们都考虑同样的事情。
  • @macias:在回应你的第一条评论时,我真的认为你是在倒退。质量正是这些限制通过消除可能导致错误的场景来改进的。假设我有一些奇怪的遗传状况,我可以无限量饮酒,并且仍然是一个完全安全的司机。我会说:“这条禁止酒后驾车的法律是一条坏法律!酒后可以开车,不要因为别人不应该做的事而惩罚我,这对我来说是荒谬的!”通过将其定为非法,可以防止大量致命事故。
猜你喜欢
  • 2021-11-20
  • 1970-01-01
  • 2015-02-27
  • 2011-06-23
  • 1970-01-01
  • 2012-12-07
  • 1970-01-01
  • 2010-11-21
相关资源
最近更新 更多