测试字符串是否为GUID而不抛出异常?

188

我希望尝试将一个字符串转换为Guid,但是我不想依赖于捕获异常(

  • 出于性能原因- 异常开销很大
  • 为了易用性- 调试器会弹出
  • 出于设计原因- 预期结果不是异常情况

换句话说,以下代码:

public static Boolean TryStrToGuid(String s, out Guid value)
{
    try
    {
        value = new Guid(s);
        return true;
    }
    catch (FormatException)
    {
        value = Guid.Empty;
        return false;
    }
}

不适合使用。

我会尝试使用正则表达式,但由于guid可能是括号包裹、大括号包裹或未包裹的形式,这使得它很难匹配。

另外,我认为某些Guid值是无效的(?)


更新1

ChristianK提出了一个很好的想法,只捕获FormatException而不是所有异常。修改问题的代码示例以包含此建议。


更新2

为什么要担心抛出的异常?我真的经常遇到无效的GUID吗?

答案是是的。这就是我使用TryStrToGuid的原因-我预计数据有问题。

示例1命名空间扩展可以通过在文件夹名称后附加GUID来指定。我可能正在解析文件夹名称,检查最终 .之后的文本是否是GUID。

c:\Program Files
c:\Program Files.old
c:\Users
c:\Users.old
c:\UserManager.{CE7F5AA5-6832-43FE-BAE1-80D14CD8F666}
c:\Windows
c:\Windows.old
示例2:我可能正在运行一个使用频率很高的Web服务器,想要检查一些发送回来的数据的有效性。我不希望无效的数据占用的资源比它需要的高出2-3个数量级。

示例3:我可能正在解析用户输入的搜索表达式。

enter image description here

如果他们输入GUID,我想特别处理它们(例如专门搜索该对象或在响应文本中突出显示和格式化该特定搜索词)。


更新3 - 性能基准测试

测试转换10,000个好的GUID和10,000个坏的GUID。

Catch FormatException:
   10,000 good:     63,668 ticks
   10,000 bad:   6,435,609 ticks

Regex Pre-Screen with try-catch:
   10,000 good:    637,633 ticks
   10,000 bad:     717,894 ticks

COM Interop CLSIDFromString
   10,000 good:    126,120 ticks
   10,000 bad:      23,134 ticks

p.s. 我不应该为一个问题辩解。


我不知道这一个的答案,但是顺便说一下,你不想在这里使用try/catch块是正确的。它需要大量计算来抵消捕获异常的成本,另外try/catch也不能用于常规程序流程!当然,如果你不需要在那段代码中捕获很多异常,这并不是什么大问题 - wprl
7
为什么这是社区维基? - Jeff
38
没错,你不应该为一个问题进行辩护。然而,我很感兴趣地阅读了这个辩护(因为它与我在这里阅读的内容非常相似)。所以,感谢你的精彩阐述。 - b w
2
@Jeff 可能是因为原帖已经被编辑了超过10次 - 请参见社区维基上的元信息 - Marijn
3
请持续关注本页面,以获取Guid.TryParse或Guid.TryParseExact的解决方案。在.NET 4.0+中,上述解决方案不够优雅。 - dplante
1
@dplante 当我最初在2008年提出这个问题时,还没有 4.0。这就是为什么问题和被接受的答案是它们现在的样子的原因。 - Ian Boyd
19个回答

113

性能基准测试

Catch exception:
   10,000 good:    63,668 ticks
   10,000 bad:  6,435,609 ticks

Regex Pre-Screen:
   10,000 good:   637,633 ticks
   10,000 bad:    717,894 ticks

COM Interop CLSIDFromString
   10,000 good:   126,120 ticks
   10,000 bad:     23,134 ticks

COM Intertop (最快)答案:

/// <summary>
/// Attempts to convert a string to a guid.
/// </summary>
/// <param name="s">The string to try to convert</param>
/// <param name="value">Upon return will contain the Guid</param>
/// <returns>Returns true if successful, otherwise false</returns>
public static Boolean TryStrToGuid(String s, out Guid value)
{
   //ClsidFromString returns the empty guid for null strings   
   if ((s == null) || (s == ""))   
   {      
      value = Guid.Empty;      
      return false;   
   }

   int hresult = PInvoke.ObjBase.CLSIDFromString(s, out value);
   if (hresult >= 0)
   {
      return true;
   }
   else
   {
      value = Guid.Empty;
      return false;
   }
}


namespace PInvoke
{
    class ObjBase
    {
        /// <summary>
        /// This function converts a string generated by the StringFromCLSID function back into the original class identifier.
        /// </summary>
        /// <param name="sz">String that represents the class identifier</param>
        /// <param name="clsid">On return will contain the class identifier</param>
        /// <returns>
        /// Positive or zero if class identifier was obtained successfully
        /// Negative if the call failed
        /// </returns>
        [DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = true)]
        public static extern int CLSIDFromString(string sz, out Guid clsid);
    }
}

总之,如果您需要检查字符串是否为 GUID,并且关心性能,请使用 COM Interop。

如果您需要将字符串表示的 GUID 转换为 Guid,请使用:

new Guid(someString);

9
你是在使用调试器的情况下运行这些代码吗?如果不使用调试器,抛出异常的性能会提高数倍。 - Daniel T.
谢谢。我本来也想问这个问题。很高兴找到了你的答案。 - David
我已经创建了一个名为PInvoke.cs的新文件,其中包含来自上面的PInvoke命名空间代码片段,但是我无法使代码正常工作。当我进行调试时,我发现CLSIDFromString的结果始终为负数。我尝试将调用行更改为: int hresult = PInvoke.ObjBase.CLSIDFromString(Guid.NewGuid().ToString(), out value); 但结果仍然始终为负数。我做错了什么? - JALLRED

95

10
更快的方法是使用Guid.TryParseExact()方法。 - user586254
7
如果解析 GUID 字符串是您应用程序中最慢的部分,那么您是幸运的。 - No Refunds No Returns

67

你可能不喜欢这个答案,但是你为什么认为捕获异常会变慢呢?

相比成功的尝试解析GUID,你预计有多少次失败的尝试呢?

我的建议是使用你刚刚创建的函数,并对你的代码进行性能分析。如果你发现这个函数确实是一个热点,那么再去修复它,但不要在此之前。


2
好的回答,过早优化是万恶之源。 - Kev
36
依赖非特殊情况的例外是不好的行为习惯,我不希望任何人养成这种习惯。特别是在图书馆例程中,我绝不希望出现这种情况,因为人们会信任它能够正常工作。请注意不要改变原意,使翻译内容更通俗易懂。 - Ian Boyd
你期望在解析GUID时有多少次失败尝试与成功尝试相比?我期望每个我解析的字符串都不是GUID。请参阅NSE文件夹根目录:http://msdn.microsoft.com/en-us/library/cc144096(VS.85).aspx - Ian Boyd
6
例外应该在“开发人员无法处理”的情况下使用。我反对微软使用“所有异常”来管理错误的方式。防御性编程原则。请微软框架开发人员考虑向 Guid 类添加“TryParse”。 - Mose
14
作为对我自己评论的回应,Guid.TryParse已被添加到4.0框架中 --- http://msdn.microsoft.com/en-us/library/system.guid_methods%28VS.100%29.aspx --- 感谢微软如此迅速地作出反应 ;) - Mose
显示剩余3条评论

47

在 .NET 4.0 中,您可以按照以下方法编写代码:

public static bool IsValidGuid(string str)
{
    Guid guid;
    return Guid.TryParse(str, out guid);
}

4
这应该真正成为最佳答案之一。 - Tom Lint

22

我至少会将其重写为:

try
{
  value = new Guid(s);
  return true;
}
catch (FormatException)
{
  value = Guid.Empty;
  return false;
}

在 SEHException、ThreadAbortException 或其他致命的或不相关的异常中,你不想说“无效的 GUID”。

更新:从 .NET 4.0 开始,Guid 有一组新的可用方法:

实际上应该使用这些方法(即使只是因为它们没有在内部 “天真地” 使用 try-catch 实现)。


14

Interop比仅捕获异常慢:

在正常情况下,有10,000个GUID:

Exception:    26ms
Interop:   1,201ms

在不幸的情况下:

Exception: 1,150ms
  Interop: 1,201ms

它更加一致,但也更加缓慢。在我看来,你最好将调试器配置为仅在未处理的异常上中断。


你的调试器不能仅在未处理的异常上中断。 - Ian Boyd
1
@Ian Boyd - 如果你正在使用任何版本的VS(包括Express),这是一个选项。http://msdn.microsoft.com/en-us/library/038tzxdw.aspx。 - Mark Brackett
1
我的意思是这不是一个可行的选项。就像“失败不是一个选项”一样。它确实是一个选项,但我不会选择它。 - Ian Boyd

10

好的,这是你需要的正则表达式...

^[A-Fa-f0-9]{32}$|^({|\\()?[A-Fa-f0-9]{8}-([A-Fa-f0-9]{4}-){3}[A-Fa-f0-9]{12}(}|\\))?$|^({)?[0xA-Fa-f0-9]{3,10}(, {0,1}[0xA-Fa-f0-9]{3,6}){2}, {0,1}({)([0xA-Fa-f0-9]{3,4}, {0,1}){7}[0xA-Fa-f0-9]{3,4}(}})$

不过这只是刚开始。您还需要验证各个部分,例如日期/时间是否在可接受范围内。我无法想象这比您已经概述的try/catch方法更快。希望您没有收到太多无效的GUID以需要进行此类检查!


如果我没记错,从时间戳生成的 GUID 通常被认为是一个坏主意,而另一种类型(类型 4)则完全是随机的。 - BCS

6

出于可用性的原因-调试器弹出

如果您采用try/catch方法,可以添加[System.Diagnostics.DebuggerHidden]属性,以确保调试器不会中断,即使您已将其设置为在引发异常时中断。


5

我遇到了类似的情况,并且发现无效字符串几乎从未达到36个字符。因此,基于这个事实,我稍微修改了您的代码,以获得更好的性能,同时保持简单。

public static Boolean TryStrToGuid(String s, out Guid value)
{

     // this is before the overhead of setting up the try/catch block.
     if(value == null || value.Length != 36)
     {  
        value = Guid.Empty;
        return false;
     }

    try
    {
        value = new Guid(s);
        return true;
    }
    catch (FormatException)
    {
        value = Guid.Empty;
        return false;
    }
}

1
Guid在其构造函数中接受的不仅仅是带破折号的字符串形式。GUID可以有带有破折号或花括号的周围,也可以没有破折号或花括号。当使用这些替代但同样有效的字符串形式时,此代码将生成错误的负面结果。 - Chris Charabaruk
1
跟进一下,字符串形式的 GUID 的有效长度分别为 32、36 和 38 -- 纯十六进制、带破折号和带括号破折号,分别对应不同格式。 - Chris Charabaruk
1
@Chris,你的观点是正确的,但是@JBrooks提出的在进入try/catch之前对潜在GUID进行健全性检查的想法是有道理的,特别是如果可疑输入很常见的话。也许可以像这样:if( value==null || value.Length < 30 || value.length > 40 ) {value=Guid.Empty;return false;} - b w
1
确实,那样会更好,不过我会保持范围更紧,使用32..38而不是30..40。 - Chris Charabaruk

5
虽然使用错误会更加昂贵,但是大多数人认为他们的GUID将由计算机生成,因此使用TRY-CATCH并不太昂贵,因为它只在CATCH阶段产生成本。你可以通过这个链接进行简单的测试。
using System.Text.RegularExpressions;


 /// <summary>
  /// Validate that a string is a valid GUID
  /// </summary>
  /// <param name="GUIDCheck"></param>
  /// <returns></returns>
  private bool IsValidGUID(string GUIDCheck)
  {
   if (!string.IsNullOrEmpty(GUIDCheck))
   {
    return new Regex(@"^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$").IsMatch(GUIDCheck);
   }
   return false;
  }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接