C#中子字符串的意外行为[重复]

Posted

技术标签:

【中文标题】C#中子字符串的意外行为[重复]【英文标题】:Unexpected behavior of Substring in C# [duplicate] 【发布时间】:2015-12-30 14:44:00 【问题描述】:

.netSystem.String类中Substring()方法的定义是这样的

public string Substring(int startIndex)

根据方法定义,startIndex“此实例中子字符串从零开始的起始字符位置”。如果我理解正确,这意味着它将给我字符串的一部分,从给定的从零开始的索引开始。

现在,如果我有一个字符串 "ABC" 并获取具有不同索引的子字符串,我会得到以下结果。

var str = "ABC";
var chars = str.ToArray(); //returns 3 char 'A', 'B', 'C' as expected

var sub2 = str.Substring(2); //[1] returns "C" as expected
var sub3 = str.Substring(3); //[2] returns "" ...!!! Why no exception??
var sub4 = str.Substring(4); //[3] throws ArgumentOutOfRangeException as expected

为什么它不会为 case [2] 抛出异常?

字符串有3个字符,所以索引是[0, 1, 2],甚至ToArray()ToCharArray()方法也按预期返回3个字符!如果我尝试使用起始索引为3Substring(),它不应该抛出异常吗?

【问题讨论】:

可能是\0 字符(标记字符串的结尾)。但我不确定.NET 是否使用它。虽然值得谷歌 第 1246 行@referencesource.microsoft.com/#mscorlib/system/string.cs,1246 感谢@AlexK。和其他人(答案)指出实现和 MSDN 文档。我可以看到框架团队是这样实现的,但对我(以及我猜的其他少数人)来说,这有点出乎意料! 快速而肮脏的答案是:.NET 知道长度为 0 的字符串是什么意思,它不知道长度为 -1 的字符串是什么意思。 这个问题是否被关闭作为重复对用户是否可以在这里找到答案绝对没有影响。事实上,如果您仔细阅读 Atwood(和其他人的)关于重复的 cmets,“拥抱重复”概念的关键是问题仍然作为重复而关闭。只是保留它们有助于用户找到他们想要或需要的答案。 【参考方案1】:

documentation 非常明确地表明这是正确的行为:

返回值:一个字符串,相当于这个实例中从 startIndex 开始的子字符串,或者 如果 startIndex 等于这个实例的长度,则为空。

如果startIndex 小于零或*大于此实例的长度,则抛出ArgumentOutOfRangeException。 *

换句话说,在最后一个字符之后取一个子字符串会给你一个空字符串。

您希望它为您提供字符串的部分的评论与此不兼容。 “字符串的一部分”也包括所有长度为零的子字符串的集合,s.substring(n, 0)给出一个空字符串。

【讨论】:

我可以看到这是“已实现”的行为,但它是不是出乎意料和令人困惑? @ArghyaC,显然只对某些人来说 :-) 请参阅我的最后一段。由于子字符串可以在 个字符之间包含零宽度实体(如果您要求长度为零),因此您也可以在最后一个字符之后获取零宽度实体。 对于最后一段,它有点道理(在某种程度上)。然后它转到nullstring.Empty 方向。但是感谢您的解释:) 我在想为什么他们决定在这种情况下返回空字符串......s.substring(n, 0)回答了我的问题! @IsmaelMiguel 显然stringsC# 中的情况并非如此,请参阅此 MSDN 文档 msdn.microsoft.com/en-us/library/system.string.aspx 中的字符串和嵌入的空字符部分【参考方案2】:

这里有很多技术性的回答说框架是如何处理方法调用的,但是我想通过类比来说明为什么它是这样的。

string 视为一个栅栏,栅栏面板本身就是角色,由栅栏柱支撑,编号如下所示:

0   1   2   3
| A | B | C |   "ABC"

0   1   2   3   4   5   6   7   8   9
| M | y |   | S | t | r | i | n | g |   "My String"

在这个类比中,string.Substring(n) 返回以栅栏n 开头的面板的string。请注意,字符串的最后一个字符后面有一个栅栏柱。使用此栅栏帖子调用该函数会返回一个值,说明此点之后没有栅栏面板(即,它返回空的string)。

同样,string.Substring(n, l) 返回一个stringl 面板,以fencepost n 开头。这就是为什么像"ABC".Substring(2, 0) 这样的东西也会返回""

【讨论】:

+1 如果将指针和索引视为标识项目之间的空间而不是标识项目本身,我认为许多与指针和索引相关的概念效果最好。第一项位于索引 0 和索引 1 之间;第二个位于 1 和 2 之间,依此类推。在字符串的情况下,有时将字符串视为后面有无限数量的栅栏是有用的(因此在许多 BASIC 版本中,例如,mid("Hello",23,1) 将完全愉快地返回一个空字符串)。我希望语言/框架作者能经常包括这两者…… ...当没有足够的栅栏时陷阱的方法以及很高兴返回较短或空字符串的方法。有时代码想说“我需要从索引 9 开始的正好 5 个字符”,但有时需要的是“如果字符串延伸那么远,我需要从索引 9 开始最多 5 个字符”。恕我直言,这两种操作都经常需要,因此值得为这两种操作使用单独的方法。 这是一个很好的类比。但是,通常index 表现为指向数组中一个内存位置/项目的指针,而不是项目之间的位置。不是吗?为什么它只对Substring 有这样的行为?如果这是一般行为,那么 "ABC".ToArray()[3] 不应该抛出 IndexOutOfRangeException 恕我直言。 @ArghyaC 我想这只是索引单个 char(其中没有空字符的概念 - 值类型不能是 null)和那些返回string(有)。或者,回到类比,.toArray() 返回一堆没有支撑栅栏的面板。【参考方案3】:

Sometimes looking at the code can be handy:

首先这被称为:

public string Substring(int startIndex)

    return this.Substring(startIndex, this.Length - startIndex);

由于减值,长度为0:

public string Substring(int startIndex, int length)

    if (startIndex < 0)
    
        throw new ...
    
    if (startIndex > this.Length)
    
        throw new ...
    
    if (length < 0)
    
        throw new ...
    
    if (startIndex > (this.Length - length))
    
         throw new ...
    
    if (length == 0) // <-- NOTICE HERE
    
        return Empty;
    
    if ((startIndex == 0) && (length == this.Length))
    
        return this;
    
    return this.InternalSubString(startIndex, length);

【讨论】:

这段代码表明这几乎是一种“决定性”的行为。对于Substring(n, 0),返回string.Empty 几乎是显而易见的,但对于Substring(lastIndex + 1)?没有那么多,恕我直言。但是,这将是一场相当自以为是的辩论:) 我同意。很奇怪 s.Substring(n) 返回s.Substring(n, s.Length - n)。所以s.Substring(lastIndex + 1) 就是(s.Substring(lastIndex + 1, 0)... @MikeEdenfield 对于(s.Substring(lastIndex + 1, 0),它返回一个空字符串,我们都知道。现在,该方法有 2 个参数,startIndex = lastIndex + 1length = 0。这种行为证明了第二个参数是合理的。但是,它看起来不是简单地忽略了第一个参数吗?否则,为什么要为 (s.Substring(lastIndex + n, 0) where n > 1 抛出异常? 我承认原因不太明显,但我相信这是有道理的。框架在概念上进行了两项检查(实际上,实现是其中大部分的捷径):1. 是否有一个字符串可供我获取子字符串,以及 2. 一个子字符串有多长我要吗?如果你要求SubString(length, 0),那么第 1 步说“是的,空字符串”,第二步说“0 长度字符串”。那些很好。但是如果你要求Substring(length + 1, 0),那么第一步会说“不,这里没有字符串可以使用。”【参考方案4】:

根据 MSDN 上写的:

*

返回值 - 与此实例中从 startIndex 开始的子字符串等效的字符串,如果 startIndex 等于此实例的长度,则为 Empty。

例外情况 ArgumentOutOfRangeException - startIndex 小于零或大于此实例的长度

*

【讨论】:

【参考方案5】:

查看String.Substring Method文档,如果起始索引等于长度,则会返回一个空字符串。

一个字符串,它等价于长度为length的子字符串 在这种情况下从 startIndex 开始,如果 startIndex 相等,则为 Empty 到这个实例的长度,长度为零。

【讨论】:

【参考方案6】:

Substring 所做的是检查 startIndex 是否大于字符串的长度,然后才抛出异常。在您的情况下,它是相等的(字符串长度为 3)。之后,它检查子字符串的长度是否为零,如果为零则返回 String.Empty。在您的情况下,子字符串的长度是字符串的长度 (3) 减去 startIndex (3)。这就是为什么子字符串的长度为0,返回一个空字符串的原因。

【讨论】:

【参考方案7】:

C#中的所有字符串最后都有String.Empty

Here is good answer关于这个问题。

来自 MSDN -String 类(系统):

在 .NET Framework 中,String 对象可以包含嵌入的 null 字符,它算作字符串长度的一部分。然而,在 某些语言,如 C 和 C++,空字符表示结束 一个字符串;它不被视为字符串的一部分,并且不是 算作字符串长度的一部分。

【讨论】:

all strings in the end have "" 是非常错误的。这就像说所有数组最后都多了一项,即数组(!)并且不包含任何项(!)。链接答案使用词 matches,而不是 haveSubstring 在请求长度为 0 的字符串时所做的事情是显而易见的 - 返回空字符串,但这不是因为它在字符串的末尾或类似的东西。 @Sinatr 我们只有在反编译库后才知道这一点 不,我们确定哪个字符串末尾没有""。在C# 中这是真的:"some string" == "some string" + "",但这并不是因为"" 被添加(并在比较过程中被忽略)或存在于末尾。这是因为当您使用"" 操作时,没有任何反应String.Empty是一种特殊情况,当0长度的字符串是运算结果时,会被字符串运算方法返回。 @Sinatr 你的 cmets 解释了一些优点。你为什么不添加作为答案,如果将来有人来这个帖子会很好。 @ArghyaC,Royi Namir 的回答已经是您问题的完美答案。我这里的 cmets 与这个答案有关,我认为这是错误的(恕我直言,我的所有 cmets 都应该删除,我的所有 cmets 都写给了作者)。【参考方案8】:

为了补充其他答案,Mono 也正确实现了此行为。

public String Substring (int startIndex)

    if (startIndex == 0)
        return this;
    if (startIndex < 0 || startIndex > this.length)
        throw new ArgumentOutOfRangeException ("startIndex");

    return SubstringUnchecked (startIndex, this.length - startIndex);


// This method is used by StringBuilder.ToString() and is expected to
// always create a new string object (or return String.Empty). 
internal unsafe String SubstringUnchecked (int startIndex, int length)

    if (length == 0)
        return String.Empty;

    string tmp = InternalAllocateStr (length);
    fixed (char* dest = tmp, src = this) 
        CharCopy (dest, src + startIndex, length);
    
    return tmp;

如您所见,如果长度为零,则返回 String.Empty。

【讨论】:

这是Mono 中一个不错的紧凑实现。是的,它在功能上类似于 FCL 实现。

以上是关于C#中子字符串的意外行为[重复]的主要内容,如果未能解决你的问题,请参考以下文章

计算字符串中子字符串的所有非重叠出现次数[重复]

C ++中子字符串方法的问题[重复]

如何在字符串中查找子字符串,其中子字符串位于 Access Query 的单独表中?

c#数组的子字符串等价物[重复]

C语言 计算字符串中子串出现的次数 求更改

C#中子窗体操作父窗体的变量