c#啥时候要给事件添加add和remove访问器

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c#啥时候要给事件添加add和remove访问器相关的知识,希望对你有一定的参考价值。

参考技术A 代码:
public event EventHandler<NewMailEventArgs> NewMail;
C#编译器编译这行代码时,会把它翻译成以下3个构造:
//1. 一个被初始化为null的私有委托字段
private EventHandler<NewMailEventArgs> NewMail = null;
//2. 一个允许对象订阅事件的公共方法add_Xxx(其中Xxx是事件的名称)
[MethodImpl(MethodImplOptions.Synchronized)]
public void add_NewMail(EventHandler<NewMailEventArgs> value)
NewMail = (EventHandler<NewMailEventArgs>)
Delegate.Combine(NewMail, value);

//3. 一个允许对象注销事件的公共方法remove_Xxx(其中Xxx是事件的名称)
[MethodImpl(MethodImplOptions.Synchronized)]
public void remove_NewMail(EventHandler<NewMailEventArgs> value)
NewMail = (EventHandler<NewMailEventArgs>)
Delegate.Remove(NewMail, value);

第一个构造只是一个适当的委托类型的字段。这个字段引用的是一个委托链表的首部,当事件发生时,链表中的委托对象将被通知。该字段被初始化为null,这意味着刚开始没有监听者订阅这个事件。当有方法订阅事件时,该字段会指向一个EventHandler <NewMailEventArgs>委托的实例,该实例还可以引用额外的EventHandler <NewMailEventArgs>委托。当监听者订阅了事件时,它只需将一个委托类型的实例添加到该委托类型的链表上即可。显然,注销事件意味着从委托链表上移除相应的委托实例。
注意,委托字段,即本例中的NewMail,尽管在源代码中将事件定义为public,但委托字段也总是为private。将委托字段定义为私有方式可以防止类定义外的代码错误地操作该字段。如果该字段为公共字段,那么任何代码都可以改变字段的值,从而删除所有已订阅事件的委托实例。
C#编译器产生的第二个构造是一个允许其他对象订阅事件的方法。C#编译器通过在事件名称 (NewMail)前添加add_自动地命名方法。C#编译器还自动地为方法产生代码。产生的代码通常调用System.Delegate的静态 Combine方法,并将委托实例添加到委托链表上,然后返回新的链表首部(得到已保存到字段中的内容)。
当C#编译器看到使用-=操作符注销事件委托的代码时,会生成一个对事件的remove方法的调用。
mm.remove_NewMail(new EventHandler<NewMailEventArgs>(FaxMsg));
和+ =操作符一样,即使使用的编程语言不直接支持事件,我们仍然可以通过显式地调用remove访问器方法来注销事件。remove方法通过扫描链表,寻找传入的密封了相同方法的委托来注销事件的委托。如果找到匹配的委托,就将其从事件的委托链表上移除。如果没有找到,也不会出现任何错误,事件的委托链表也不会有任何改变。
顺便说一下,C#要求代码使用+=和-=操作符在链表上添加和移除委托。如果我们试图显式地调用add和remove方法,那么C#编译器将生成一个错误消息:CS0571: “不能显式调用操作符或访问器方法”。
C# 编译器为事件的add和remove方法增加[MethodImpl (MethodImplOptions.Synchronized)]属性。这个属性的目的是为了确保在操作实例的事件成员时,对于任何一个对象,在同一时刻只能有一个add方法或者remove方法可以执行。该属性同样确保在操作静态事件成员时,同一时刻也只能有一个add方法或者remove方法可以执行。这里需要线程同步,以免委托对象的链表被破坏。但应注意,CLR实现线程同步的方式中存在许多问题。
对实例(非静态)方法应用 MethodImpl属性时,CLR使用对象本身作为线程同步锁。这意味着如果类定义了许多事件,那么所有的add和remove方法都将使用相同的锁,这种情况下,如果有多个线程同时订阅和注销不同的事件,则会损害可扩展性。但这种情况非常少见,而且对于大多数应用程序,这并不是一个问题。但是,线程同步的指导方针中规定方法不应在对象本身上加同步锁,因为同步锁将对所有的代码公开。这意味着任何人都可以编写代码锁住这个对象,从而可能导致其他线程死锁。如果希望自己编写的类型防御措施好,更加健壮,那么应使用不同的对象来完成加锁功能。10.5节将示范如何实现这个
功能。
在静态方法上应用[MethodImpl(MethodImplOptions.Synchronized)]属性时,CLR使用类型对象作为线程同步锁。这又意味着如果类定义了许多静态事件,那么所有的add和remove方法都将使用相同的锁,这种情况下,如果有多个线程同时订阅和注销不同的事件,则会损害代码的性能。但是这种情况也非常少见。
但还有一个严重的问题:线程同步指导方针指出,方法永远不要在类型对象上加锁,因为这个锁将对所有的代码公开。另外,当类型加载与域无关时,CLR中还有一个错误。这种情况会导致同步锁被使用该类型的所有应用程序域共享,如此一来,一个程序域中的代码会影响另一个应用程序域中运行的代码。实际中,C#编译器在实现静态add和remove方法的线程安全时应采用完全不同的方法。10.5节将讨论一种修正C#编译器这一缺陷的机制。
C#和CLR允许定义一个有一个或者多个实例 (非静态)事件成员的值类型(结构)。但必须意识到,在上述情况下,C#编译器根本不会保证线程安全。因为拆箱(unboxing)的值类型没有与其相关联的锁对象。实际上,C#编译器不为add和remove方法生成[MethodImpl (MethodImplOptions.Synchronized)]属性,因为该属性对值类型的实例方法没有效果。遗憾的是,当实例事件定义为值类型的成员时,的确没有更好的方式为它们保证线程安全。因此,建议大家尽量避免这样做。注意,在值类型中定义的静态事件(加上前面讨论的限制)可以保证线程安全,因为静态事件对类型对象(引用类型)本身加锁。但是,如果要使代码健壮,应采用10.5节讨论的机制。
有时我们会感到编译器生成的 add和remove方法不是那么理想。例如10.4节中讨论的使用MicrosoftC#编译器时遇到的所有线程安全问题。实际上,Microsoft 的C#编译器在安全编程(defensive coding)和健壮性方面永远不是最安全的。为了创建一个坚固的组件,建议经常采用本节介绍的技术,该技术可以用于解决与线程安全相关的所有问题。而且该技术同样也可以应用于其他目的。例如,显式实现add和remove方法的普遍原因就是类型定义了许多事件,而且又需要高效地进行存储。有关详情请参见 10.6节。
幸亏C#编译器以及其他许多编译器都允许开发人员显式地实现add和remove访问器方法。为了保证MailManager对象上事件的订阅和注销的线程安全,我们修改了MailManager类的代码,修改后的代码如下所示:

internal class MailManager
//创建一个作为线程同步锁的私有实例字段
private readonly Object m_eventLock = new Object();
//增加一个引用委托链表头部的私有字段
private EventHandler<NewMailEventArgs> m_NewMail;
//为类增加一个事件成员
public event EventHandler<NewMailEventArgs> NewMail
//显式实现'add'方法
add
//加私有锁,并向委托链表增加一个处理程序(以'value'为参数)
lock (m_eventLock) m_NewMail += value;

//显式实现'remove'方法
remove
//加私有锁,并从委托链表从中移除处理程序(以'value'为参数)
lock (m_eventLock) m_NewMail -= value;

对List遍历过程中添加和删除的思考

对List遍历过程中添加和删除的思考

经过测试,发现对于一个集合,如果要在遍历的过程中,进行add或者remove操作的时候,如果操作不慎,都会导致报错。

主要错误:

  • java.util.ConcurrentModificationException 遍历过程中add/remove导致的错误

  • java.lang.IndexOutOfBoundsException 越界错误

最佳实践

add操作:利用原生的for循环。remove操作利用foreach操作。如下所示:

    //OK,利用 iterator 和 其remove 方法
   @Test
   public void testIteratorRemove2() {
       Iterator<String> iterator = list.iterator();
       while (iterator.hasNext()) {
           if ("3".equals(iterator.next())) {
               iterator.remove();
          }
      }
       System.out.println(list);
  }
?
   //OK 利用for循环。
   @Test
   public void testForAdd() {
       for (int i = 0, length = list.size(); i < length; i++) {
           if (list.get(i).equals("2")) {
               list.add("2");
          }
      }
  }

经典错误1

如下代码本意是:通过iterator的方式从头到尾变遍历list中的元素。

    @Test
   public void testIteratorRemove2() {
       while (list.iterator().hasNext()) {
         System.out.println(list.iterator().next());
      }
  }

但是该段代码永远都会输出 list的第一元素,为什么?关键错误在链式写法上:

 while (list.iterator().hasNext()) {}

每次循环时候先调用了list.iterator() 在该方法中每次都是重新new了一个新的对象


   public Iterator<E> iterator() {
       return new Itr();
  }

所以每一次都是一个新的遍历对象,所以输出第一个元素。

那么为什么每次都要new一个新的Itr()?我猜想应该是为了并发的读,每次读的都是一份独立的数据,避免多个并发读的时候,出现当前指针问题。

处理办法:将list.iteraotr() 放在外面即可,保证循环中循环的是1个对象。


   @Test
   public void testIteratorRemove2() {
       Iterator<String> iterator = list.iterator();
       while (iterator.hasNext()) {
           if ("3".equals(iterator.next())) {
               iterator.remove();
          }
      }
       System.out.println(list);
  }

 

经典错误2

    // 死循环
@Test
   public void testForAdd2() {
       for (int i = 0; i < list.size(); i++) {
           if (list.get(i).equals("3")) {
               list.add("3");
          }
      }
  }

当if条件满足的时候,该方法永远不会结束,为什么?

对于for循环 for (int i = 0; i < list.size(); i++)有3个部分,第一个部分为初始化,只执行一次。第二个部分每次都会执行,第三个部分也是每次都会执行。

上述问题的第二步会导致无限循环:因为for中每一次循环都会在list添加了一个元素,每次步进为1,内部元素也是每次都加1.

如何处理该问题: list.size()放在第一部分,第一部分只初始化一次。


   //OK 利用for循环。
   @Test
   public void testForAdd() {
       for (int i = 0, length = list.size(); i < length; i++) {
           if (list.get(i).equals("2")) {
               list.add("2");
          }
      }
  }

经典错误3

    //java.lang.IndexOutOfBoundsException: Index: 2, Size: 2
   @Test
   public void testForRemove() {
       for (int i = 0, length = list.size(); i < length; i++) {
           if (list.get(i).equals("3")) {
               list.remove("3");
          }
      }
  }

该错误在list.add("3")的时候就不会发生该错误,具体原因是什么?

以上是关于c#啥时候要给事件添加add和remove访问器的主要内容,如果未能解决你的问题,请参考以下文章

tomcat右键没有add and remove 还有啥办法能找出add and remove? 在线等

编写高质量代码改善C#程序的157个建议——建议151:使用事件访问器替换公开的事件成员变量

c# datagridview 单个 单元格的样式(即字号或者字体颜色)发生改变时候,触发啥事件???

请问各位HTML高手 addClass和append都是添加的意思 它们俩有啥区别呢 都是啥时候用呢

C#中啥时候用小驼峰命名“camelCap”,啥时候用下划线命名“button1_Click”?

Python可迭代对象中的添加和删除(add,append,pop,remove,insert)