dotnet C# 基础 为什么 GetHashCode 推荐只取只读属性或字段做哈希值
Posted lindexi
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了dotnet C# 基础 为什么 GetHashCode 推荐只取只读属性或字段做哈希值相关的知识,希望对你有一定的参考价值。
在 C# 里面,所有的对象都继承 Object 类型,此类型有开放 GetHashCode 用于给开发者重写。此 GetHashCode 方法推荐是在重写 Equals 方法时也同时进行重写,要求两个对象在 Equals 返回相等时,两个对象的 GetHashCode 返回值也相等。反过来则不然,允许有两个不相等的对象的 GetHashCode 是相等的
在重写 Equals 方法时,大部分时候都是自动生成的,如将类里面的所有字段或属性都进行一一比较。那在 GetHashCode 方法里面,所输出的哈希值的计算,是否也需要使用此类型的所有字段或属性共同计算出来?如果在 GetHashCode 里面使用的字段或属性非只读,那么 ReSharper 将会警告你这是不安全的。本文将来告诉大家为什么这是不安全的
在 dotnet 里面,大部分会用到 GetHashCode 的逻辑都在于哈希容器里面,如 Dictionary 字典等。这些哈希容器在设计上都期望类型遵守以下行为:当两个对象相等的时候,那么获取 GetHashCode 的值也一定相等
假定有类型的 GetHashCode 返回值是基于非只读的属性或字段,将会导致在将对象加入哈希容器的时候,所获取到的 GetHashCode 的值是不包括未来对非只读属性或字段变更的防御的。在未来对此对象的非只读的属性或字段进行变更,也许就会影响到此对象再次获取 GetHashCode 的属性,从而让相同的一个对象,在哈希容器里面,因为 GetHashCode 返回值不同,而被认为是不同的对象
假设有如此的代码逻辑,某个 Foo2 的对象的 GetHashCode 返回值是由此对象的属性决定的,如下面代码
class Foo2
{
public int HashCode { set; get; }
public override int GetHashCode()
{
return HashCode;
}
}
假定将此 Foo2 的对象加入到字典里面,接着去判断字典里面是否存在此对象。再修改 Foo2 的 HashCode 属性,再去判断字典里面是否存在此对象,代码如下
var foo2 = new Foo2();
Dictionary<Foo2, object> dictionary = new();
dictionary[foo2] = foo2;
Console.WriteLine(dictionary.ContainsKey(foo2));
foo2.HashCode = 2;
Console.WriteLine(dictionary.ContainsKey(foo2));
有趣的逻辑是第一次返回的符合预期,就是 True 的值。然而第二次,明明没有从字典里面移除 Foo2 对象,然而字典却认为找不到此对象
其原因如上文,在字典里面,优先通过 GetHashCode 的值来进行判断。如上面代码,更改了 Foo2 的 GetHashCode 返回值,将会让字典找不到此 HashCode 对应的元素,从而让字典认为不存在此对象
大部分在设计类型的时候,都不会考虑到某个类型在未来或其他模块里面,会被存放进哈希容器里面。如果此时在 GetHashCode 里面,使用了非只读字段或属性,将会挖一个坑。也许某个逻辑变更了这些非只读字段或属性的时候,影响了 GetHashCode 的返回值从而影响了哈希容器的行为
这就是为什么 ReSharper 警告不要在 GetHashCode 里面使用非只读字段或属性进行制作哈希值的原因
不过在理解了这个行为,在某些特别的业务里面,也可以利用此行为实现有趣的功能
通过本文也可以了解到,对于 GetHashCode 的返回值也不能为了因为重写 Equals 方法而被 VS 警告而随便写此方法的实现,如下面逗比代码
class Foo
{
public Foo(string name)
{
Name = name;
}
public string Name { get; }
public override int GetHashCode()
{
return _random.Next();
}
private readonly Random _random = new Random();
}
上面的代码在 GetHashCode 随机返回一个值,这将会让所有哈希容器炸掉,如下面的代码,将在字典拿不到值
try
{
Foo[] foo = new Foo[100];
for (int i = 0; i < 100; i++)
{
foo[i] = new Foo(i.ToString());
}
Dictionary<Foo, string> dictionary = foo.ToDictionary(t => t, t => t.Name);
for (int i = 0; i < foo.Length; i++)
{
Console.WriteLine($"{foo[i].Name}-{dictionary[foo[i]]}"); // KeyNotFoundException
}
}
catch (Exception)
{
}
可以通过如下方式获取本文的源代码,先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 572e405d383c69929397a583102576e2e140f1fc
以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
获取代码之后,进入 BelwheaheajeachelYikaidairnay 文件夹
博客园博客只做备份,博客发布就不再更新,如果想看最新博客,请到 https://blog.lindexi.com/
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名[林德熙](http://blog.csdn.net/lindexi_gd)(包含链接:http://blog.csdn.net/lindexi_gd ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我[联系](mailto:lindexi_gd@163.com)。
C# 极限压缩 dotnet core 控制台发布文件
每次发布 dotnet core 应用都会觉得发布文件太大,而如果使用极限压缩,用 CoreRT 能让发布的控制台文件到 5KB 左右,不需要带框架就能在其他设备运行
这是微软 MichalStrehovsky 大佬,也就是 CoreRT 项目开发者给的方法
在开始写代码之前,需要定义一些基础的类,因为不包含任何运行环境,所以基础的 object 这些都需要重新定义,这里的代码放在 github 我也在本文最后贴出代码
现在输出控制台的代码不是原先的 Console.WriteLine 而是通过底层方法
unsafe class Program
{
[DllImport("api-ms-win-core-processenvironment-l1-1-0")]
static extern IntPtr GetStdHandle(int nStdHandle);
[DllImport("api-ms-win-core-console-l1-1-0")]
static extern IntPtr WriteConsoleW(IntPtr hConsole, void* lpBuffer, int charsToWrite, out int charsWritten, void* reserved);
static int Main()
{
string hello = "Hello world!";
fixed (char* c = hello)
{
int charsWritten;
WriteConsoleW(GetStdHandle(-11), c, hello.Length, out charsWritten, null);
}
return 42;
}
}
最难的是如何编译这个文件
编译需要先使用 csc 编译为 IL 代码,然后通过 ilcompiler 编译为obj文件,然后通过 link 编译为运行文件
从开始菜单找到 x64 Native Tools Command Prompt for VS 2019 然后进入上面代码所在文件夹,执行下面代码编译
csc /debug:embedded /noconfig /nostdlib /runtimemetadataversion:v4.0.30319 zerosharp.cs /out:zerosharp.ilexe /langversion:latest /unsafe
编译完成可以看到 zerosharp.ilexe 文件,然后通过 ilcompiler 将这个文件编译为 zerosharp.map 和 zerosharp.obj 文件
%appdata%..\..\..\.nuget\packages\runtime.win-x64.microsoft.dotnet.ilcompiler
找到里面的最新版本,在文件夹里面的 tools 文件夹可以找到 ilc.exe 文件,如在我电脑的的文件是
c:\Users\lindexi\.nuget\packages\runtime.win-x64.microsoft.dotnet.ilcompiler\1.0.0-alpha-27606-05\tools\ilc.exe
记下这个路径,接下来将使用这个工具编译
>c:\Users\lindexi\.nuget\packages\runtime.win-x64.microsoft.dotnet.ilcompiler\1.0.0-alpha-27606-05\tools\ilc.exe zerosharp.ilexe -o zerosharp.obj --systemmodule zerosharp --map zerosharp.map -O
然后用 link 连接
link /subsystem:console zerosharp.obj /entry:__managed__Main kernel32.lib /merge:.modules=.pdata /incremental:no
执行上面代码就可以编译 zerosharp.exe 文件,这个文件只有5KB可以将这个程序放在其他设备运行
下面是所有代码
using System;
using System.Runtime.InteropServices;
#region A couple very basic things
namespace System
{
public class Object { IntPtr m_pEEType; }
public struct Void { }
public struct Boolean { }
public struct Char { }
public struct SByte { }
public struct Byte { }
public struct Int16 { }
public struct UInt16 { }
public struct Int32 { }
public struct UInt32 { }
public struct Int64 { }
public struct UInt64 { }
public struct IntPtr { }
public struct UIntPtr { }
public struct Single { }
public struct Double { }
public abstract class ValueType { }
public abstract class Enum : ValueType { }
public struct Nullable<T> where T : struct { }
public sealed class String { public readonly int Length; }
public abstract class Array { }
public abstract class Delegate { }
public abstract class MulticastDelegate : Delegate { }
public struct RuntimeTypeHandle { }
public struct RuntimeMethodHandle { }
public struct RuntimeFieldHandle { }
public class Attribute { }
namespace Runtime.CompilerServices
{
public class RuntimeHelpers
{
public static unsafe int OffsetToStringData => sizeof(IntPtr) + sizeof(int);
}
}
}
namespace System.Runtime.InteropServices
{
public sealed class DllImportAttribute : Attribute
{
public DllImportAttribute(string dllName) { }
}
}
#endregion
#region Things needed by ILC
namespace System
{
namespace Runtime
{
internal sealed class RuntimeExportAttribute : Attribute
{
public RuntimeExportAttribute(string entry) { }
}
}
class Array<T> : Array { }
}
namespace Internal.Runtime.CompilerHelpers
{
using System.Runtime;
class StartupCodeHelpers
{
[RuntimeExport("RhpReversePInvoke2")]
static void RhpReversePInvoke2() { }
[RuntimeExport("RhpReversePInvokeReturn2")]
static void RhpReversePInvokeReturn2() { }
[System.Runtime.RuntimeExport("__fail_fast")]
static void FailFast() { while (true) ; }
[System.Runtime.RuntimeExport("RhpPInvoke")]
static void RphPinvoke() { }
[System.Runtime.RuntimeExport("RhpPInvokeReturn")]
static void RphPinvokeReturn() { }
}
}
#endregion
unsafe class Program
{
[DllImport("api-ms-win-core-processenvironment-l1-1-0")]
static extern IntPtr GetStdHandle(int nStdHandle);
[DllImport("api-ms-win-core-console-l1-1-0")]
static extern IntPtr WriteConsoleW(IntPtr hConsole, void* lpBuffer, int charsToWrite, out int charsWritten, void* reserved);
static int Main()
{
string hello = "Hello world!";
fixed (char* c = hello)
{
int charsWritten;
WriteConsoleW(GetStdHandle(-11), c, hello.Length, out charsWritten, null);
}
return 42;
}
}
本文会经常更新,请阅读原文: https://blog.lindexi.com/post/C-%E6%9E%81%E9%99%90%E5%8E%8B%E7%BC%A9-dotnet-core-%E6%8E%A7%E5%88%B6%E5%8F%B0%E5%8F%91%E5%B8%83%E6%96%87%E4%BB%B6.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
以上是关于dotnet C# 基础 为什么 GetHashCode 推荐只取只读属性或字段做哈希值的主要内容,如果未能解决你的问题,请参考以下文章