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