C# 如果我将对事件对象的引用复制到另一个对象,然后更改事件对象,会怎么样?

C# 如果我将对事件对象的引用复制到另一个对象,然后更改事件对象,会怎么样?,c#,.net,events,delegates,clr,C#,.net,Events,Delegates,Clr,我正在读一本关于C#和CLR的书,a一点也不懂。全文如下: 为了修复这种竞争条件,许多开发人员编写了OnNewMail方法,如下所示: //版本2 受保护的虚拟void OnNewMail(NewMailEventArgs e){ EventHandler temp=NewMail; 如果(temp!=null)temp(this,e); } 这里的想法是将对NewMail的引用复制到临时变量temp中,temp在执行赋值时引用委托链。现在,这个方法比较temp和null并调用temp,所以在

我正在读一本关于C#和CLR的书,a一点也不懂。全文如下:

为了修复这种竞争条件,许多开发人员编写了OnNewMail方法,如下所示:

//版本2
受保护的虚拟void OnNewMail(NewMailEventArgs e){
EventHandler temp=NewMail;
如果(temp!=null)temp(this,e);
}
这里的想法是将对NewMail的引用复制到临时变量temp中,temp在执行赋值时引用委托链。现在,这个方法比较temp和null并调用temp,所以在分配给temp之后,如果另一个线程更改了NewMail,这并不重要。记住,委托是不可变的,这就是为什么这种技术在理论上是有效的


因此,问题是:为什么它会起作用?我认为,temp和NewMail对象是指同一个对象,不需要更改任何事项——结果将对两者都产生影响。谢谢

CLR中的委托类型尤其是
多播委托
类型,尽管它们是引用类型,但属于罕见的一组类型,称为“不可变”类型。这意味着此类类型的引用赋值操作创建实例的副本,而常规引用类型的赋值只复制引用的值。所以当你写作时:

EventHandler<NewMailEventArgs> temp = NewMail;
EventHandler temp=NewMail;

创建由
NewMail
引用的代理的新副本,并将对该副本的引用分配给
temp
变量,因此,在执行这一行之后,
EventHandler
委托将有两个实例:由
NewMail
引用的实例和由
temp
引用的另一个实例(不是您可能认为的由两个变量引用的单个实例)。这就是为什么现在可以安全地调用由
temp
指向的委托,因为在调用委托的一段时间内,它不能被另一个线程设置为null

避免并发问题的可靠方法是使用System.Threading.Volatile.Read方法。可以通过未记录的编译器优化删除临时变量。目前它运行良好,但将来可能会改变

using System;
using System.Threading;

namespace EventHandling
{
    class Program
    {
        static void Main(string[] args)
        {
            var eventProvider = new EventProvider();
            eventProvider.Event += (sender, e) => Console.WriteLine("Event fired");
            eventProvider.FireEvent();
        }
    }

    class EventProvider
    {
        public event EventHandler Event;

        protected void OnEvent(EventArgs e) => Volatile.Read(ref Event)?.Invoke(this, e);

        public void FireEvent() => OnEvent(EventArgs.Empty);
    }
}
要探索并发如何影响事件处理,可以尝试以下代码:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace EventHandling
{
    class Program
    {
        static void Main(string[] args)
        {
            while(true)
                new Program().Run();
        }

        private void Run()
        {
            var eventProvider = new EventProvider();
            eventProvider.Event += HandleEvent;
            Console.WriteLine("subscribed");

            var unsubscribe = new Task(() =>
            {
                eventProvider.Event -= HandleEvent;
                Console.WriteLine("unsubscribed");
            });

            var fireEvent = new Task(() => eventProvider.FireEvent());

            fireEvent.Start();
            unsubscribe.Start();

            Task.WaitAll(fireEvent, unsubscribe);

            Console.ReadLine();
        }

        private void HandleEvent(object sender, EventArgs e) => Console.WriteLine("Event fired");


    }

    class EventProvider
    {
        public event EventHandler Event;

        protected void OnEvent(EventArgs e)
        {
            var temp = Volatile.Read(ref Event);
            Console.WriteLine("temp delegate created");
            Thread.Sleep(25); // time to unsubscribe concurrently

            if (temp != null)
            {
                Console.WriteLine("temp delegate invoking");
                temp.Invoke(this, e);
                Console.WriteLine("temp delegate invoked");
            }
            else
                Console.WriteLine("temp delegate is empty");
        }

        public void FireEvent() => OnEvent(EventArgs.Empty);
    }
}
有时输出为:

subscribed
unsubscribed
temp delegate created
temp delegate is empty
有时:

subscribed
temp delegate created
unsubscribed
temp delegate invoking
Event fired
temp delegate invoked

如果你能提供一个。另外,请明确书的名称和页码。它们指的是同一个对象,但它们是两个独立的变量(它们只是内存中的两个位置,其值在某个时间点恰好相同,并且两个值都引用原始对象占用的内存)。如果将一个变量的值更改为不同的值,则另一个变量仍然具有对象的值(引用)
temp和NewMail对象引用同一对象
No,它们是指向同一对象的两个变量。如果
NewMail
被分配给一个新对象,那么您将有两个变量指向两个不同的对象。分配给一个变量不会影响另一个变量。好的,我想我应该阅读更多关于分配额的内容。谢谢。您需要更好地理解1)
Volatile
不是防弹的。领先的并发专家。2) 示例中使用的将生成包含临时变量的代码。换句话说,它使用相同的方法来修复比赛条件。@TheodorZoulias,谢谢你的文章。很有趣。临时变量本身不是问题所在。该点是不稳定的。读取可防止编译器将其删除。杰弗里·里克特(Jeffrey Richter)在其著作《CLR通过C#》中提出了解决方案。
subscribed
temp delegate created
unsubscribed
temp delegate invoking
Event fired
temp delegate invoked