dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.

Home Page:https://docs.microsoft.com/dotnet/core/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ConcurrentDictionary<TKey,TValue> When key is delegate, `AddOrUpdate` was wrong

Cricle opened this issue · comments

Description

.NET SDK:
 Version:           8.0.200
 Commit:            438cab6a9d
 Workload version:  8.0.200-manifests.5638171e

运行时环境:
 OS Name:     Windows
 OS Version:  10.0.19045
 OS Platform: Windows
 RID:         win-x64
 Base Path:   C:\Program Files\dotnet\sdk\8.0.200\

已安装 .NET 工作负载:
 [maui]
   安装源文件: SDK 8.0.200
   清单版本:    8.0.6/8.0.100
   清单路径:       C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.maui\8.0.6\WorkloadManifest.json
   安装类型:              Msi

 [maui-windows]
   安装源文件: VS 17.9.34622.214
   清单版本:    8.0.6/8.0.100
   清单路径:       C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.maui\8.0.6\WorkloadManifest.json
   安装类型:              Msi

 [maccatalyst]
   安装源文件: VS 17.9.34622.214
   清单版本:    17.2.8022/8.0.100
   清单路径:       C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.maccatalyst\17.2.8022\WorkloadManifest.json
   安装类型:              Msi

 [ios]
   安装源文件: VS 17.9.34622.214
   清单版本:    17.2.8022/8.0.100
   清单路径:       C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.ios\17.2.8022\WorkloadManifest.json
   安装类型:              Msi

 [android]
   安装源文件: VS 17.9.34622.214
   清单版本:    34.0.79/8.0.100
   清单路径:       C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.android\34.0.79\WorkloadManifest.json
   安装类型:              Msi


Host:
  Version:      8.0.2
  Architecture: x64
  Commit:       1381d5ebd2

.NET SDKs installed:
  7.0.202 [C:\Program Files\dotnet\sdk]
  8.0.100 [C:\Program Files\dotnet\sdk]
  8.0.200 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 6.0.27 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 7.0.4 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 7.0.16 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 8.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 8.0.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 6.0.27 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 7.0.4 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 7.0.16 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 8.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 8.0.2 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 6.0.27 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 7.0.4 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 7.0.16 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 8.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 8.0.2 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

Other architectures found:
  x86   [C:\Program Files (x86)\dotnet]
    registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]

Environment variables:
  Not set

global.json file:
  Not found

Learn more:
  https://aka.ms/dotnet/info

Download .NET:
  https://aka.ms/dotnet/download

Reproduction Steps

using System.Collections.Concurrent;

Console.WriteLine(new EventHandler<string>((_, __) => { }) == new EventHandler<string>((_, __) => { }));
Console.WriteLine(new EventHandler<string>((_, __) => { }).Equals(new EventHandler<string>((_, __) => { })));

var map = new ConcurrentDictionary<EventHandler<string>, bool>();
for (int i = 0; i < 2; i++)
{
    map.AddOrUpdate(new EventHandler<string>((_, __) => { }), false, (_, __) =>
    {
        Console.WriteLine("same!");
        return __;
    });
}

Expected behavior

The output was

False
False

Because the == and Equals was false, the ConcurrentDictionary key match was not same key.

Actual behavior

The output was

False
False
same!

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

No response

It is not something for ConcurrentDictionary, but the implementation detail of lambda expression.

If you replace the lambda expressions with named method, the Equals will print true:

using System;
using System.Collections.Concurrent;

void M(object? sender, string e) { }

Console.WriteLine(new EventHandler<string>(M) == new EventHandler<string>(M));
Console.WriteLine(new EventHandler<string>(M).Equals(new EventHandler<string>(M)));

var map = new ConcurrentDictionary<EventHandler<string>, bool>();
for (int i = 0; i < 2; i++)
{
    map.AddOrUpdate(new EventHandler<string>(M), false, (_, __) =>
    {
        Console.WriteLine("same!");
        return __;
    });
}

Delegate.Equals is based on the actual method behind, because it's too verbose to compare method bodies. For lambda expressions, C# compiler creates one method per call site. It does not try to deduplicate method bodies either, but it knows that the same call site within a loop is using the same method. So your code is actually compiled down like this:

Console.WriteLine(new EventHandler<string>($LambdaMethod1) == new EventHandler<string>($LambdaMethod2));
Console.WriteLine(new EventHandler<string>($LambdaMethod3).Equals(new EventHandler<string>($LambdaMethod4)));

var map = new ConcurrentDictionary<EventHandler<string>, bool>();
for (int i = 0; i < 2; i++)
{
    map.AddOrUpdate(new EventHandler<string>($LambdaMethod5), false, (_, __) =>
    {
        Console.WriteLine("same!");
        return __;
    });
}

You can also observe this by creating an array of delegates with loop:

EventHandler<string>[] delegates = new EventHandler<string>[3];
for (int i = 0; i< delegates.Length; i++)
{
    delegates[i] = (_, __) => { };
}

Console.WriteLine(delegates[0] == delegates[1]); // True
Console.WriteLine(delegates[0].Equals(delegates[2])); // True