在我们的应用程序中,我们正在创建具有 Guid 值的属性的 Xml 文件。该值需要在文件升级之间保持一致。因此,即使文件中的其他所有内容发生变化,属性的 guid 值也应保持不变。
一个明显的解决方案是创建一个静态字典,其中包含文件名和要使用的 GUID。然后每当我们生成文件时,我们都会在字典中查找文件名并使用相应的 guid。但这是不可行的,因为我们可能会扩展到 100 个文件,并且不想维护大量的 guid。
所以另一种方法是根据文件的路径使Guid相同。由于我们的文件路径和应用程序目录结构是唯一的,因此该路径的 Guid 应该是唯一的。因此,每次我们运行升级时,文件都会根据其路径获得相同的 guid。我找到了一种很酷的方法来生成这样的“确定性指南”(感谢 Elton Stoneman)。它基本上是这样做的:
private Guid GetDeterministicGuid(string input)
{
//use MD5 hash to get a 16-byte hash of the string:
MD5CryptoServiceProvider provider = new MD5CryptoServiceProvider();
byte[] inputBytes = Encoding.Default.GetBytes(input);
byte[] hashBytes = provider.ComputeHash(inputBytes);
//generate a guid from the hash:
Guid hashGuid = new Guid(hashBytes);
return hashGuid;
}
因此,给定一个字符串,Guid 将始终相同。
还有其他方法或推荐的方法来做到这一点吗?这种方法有什么优点或缺点?
正如 @bacar 所提到的,RFC 4122 §4.3 定义了一种创建基于名称的 UUID 的方法。这样做的优点(相对于仅使用 MD5 哈希)是保证它们不会与基于非命名的 UUID 发生冲突,并且与其他基于名称的 UUID 发生冲突的可能性非常(非常)小。
.NET Framework 中没有对创建这些内容的本机支持,因此我创建了实现该算法的 NGuid 包。它可以按如下方式使用:
var guid = GuidHelpers.CreateFromName(GuidHelpers.UrlNamespace, filePath);
为了进一步降低与其他 GUID 冲突的风险,您可以创建一个私有 GUID 用作命名空间 ID(而不是使用 RFC 中定义的 URL 命名空间 ID)。
这会将任何字符串转换为 Guid,而无需导入外部程序集。
public static Guid ToGuid(string src)
{
byte[] stringbytes = Encoding.UTF8.GetBytes(src);
byte[] hashedBytes = new System.Security.Cryptography
.SHA1CryptoServiceProvider()
.ComputeHash(stringbytes);
Array.Resize(ref hashedBytes, 16);
return new Guid(hashedBytes);
}
有很多更好的方法来生成唯一的 Guid,但这是一种将字符串数据密钥一致升级为 Guid 数据密钥的方法。
正如 Rob 提到的,你的方法不会生成 UUID,它会生成一个看起来像 UUID 的哈希值。
关于 UUID 的 RFC 4122 特别允许确定性(基于名称)UUID - 版本 3 和 5 使用 md5 和 SHA1(分别)。大多数人可能熟悉版本 4,它是随机的。 维基百科 很好地概述了这些版本。 (请注意,此处使用的“版本”一词似乎描述了 UUID 的“类型” - 版本 5 并不取代版本 4)。
似乎有一些库可用于生成版本 3/5 UUID,包括 python uuid module、boost.uuid(C++)和 OSSP UUID。 (我没有寻找任何.net的)
您需要区分类的实例
Guid
和全局唯一的标识符。 “确定性 guid”实际上是一个哈希(如您对 provider.ComputeHash
的调用所证明的那样)。与通过 Guid.NewGuid
创建的 Guid 相比,哈希发生冲突的可能性要高得多(两个不同的字符串恰好产生相同的哈希)。
因此,您的方法的问题在于您必须接受两条不同路径产生相同 GUID 的可能性。如果您需要一个对于任何给定路径字符串都是唯一的标识符,那么最简单的方法是只需使用该字符串。如果您需要对用户隐藏该字符串,请对其进行加密 - 您可以使用 ROT13 或更强大的东西...
尝试将非纯 GUID 的内容硬塞到 GUID 数据类型中可能会导致将来出现维护问题...
MD5 很弱,我相信你可以用 SHA-1 做同样的事情并得到更好的结果。
顺便说一句,只是个人意见,将 md5 散列装扮成 GUID 并不能使它成为一个好的 GUID。 GUID 本质上是非确定性的。这感觉就像是一个骗子。为什么不直截了当地说它是输入的字符串呈现的哈希值。您可以使用此行而不是新的引导行来做到这一点:
string stringHash = BitConverter.ToString(hashBytes)
这是一个非常简单的解决方案,对于单元/集成测试之类的事情来说应该足够好了:
var rnd = new Random(1234); // Seeded random number (deterministic).
Console.WriteLine($"{rnd.Next(0, 255):x2}{rnd.Next(0, 255):x2}{rnd.Next(0, 255):x2}{rnd.Next(0, 255):x2}-{rnd.Next(0, 255):x2}{rnd.Next(0, 255):x2}-{rnd.Next(0, 255):x2}{rnd.Next(0, 255):x2}-{rnd.Next(0, 255):x2}{rnd.Next(0, 255):x2}-{rnd.Next(0, 255):x2}{rnd.Next(0, 255):x2}{rnd.Next(0, 255):x2}{rnd.Next(0, 255):x2}{rnd.Next(0, 255):x2}{rnd.Next(0, 255):x2}");
ARM/bicep 实现了这样的方法: https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-functions-string#guid
虽然效率低且受静态编译保护较少,但可以从 .NET 代码调用 ARM 函数:
using Azure.Deployments.Expression.Expressions;
using Newtonsoft.Json.Linq;
public static string Guid(params string[] values)
{
var parameters =
values.Select(arg => new FunctionArgument(JToken.FromObject(arg))).ToArray();
return ExpressionBuiltInFunctions.Functions
.EvaluateFunction("guid", parameters, null).Value<string>()!;
}