1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 using System
.Diagnostics
;
6 using System
.Globalization
;
8 using System
.Runtime
.CompilerServices
;
9 using System
.Runtime
.InteropServices
;
11 using Internal
.Runtime
.CompilerServices
;
13 #pragma warning disable SA1121 // explicitly using type aliases instead of built-in types
15 using nuint
= System
.UInt64
;
17 using nuint
= System
.UInt32
;
22 public partial class String
25 // Search/Query methods
28 [MethodImpl(MethodImplOptions
.AggressiveInlining
)]
29 private static bool EqualsHelper(string strA
, string strB
)
31 Debug
.Assert(strA
!= null);
32 Debug
.Assert(strB
!= null);
33 Debug
.Assert(strA
.Length
== strB
.Length
);
35 return SpanHelpers
.SequenceEqual(
36 ref Unsafe
.As
<char, byte>(ref strA
.GetRawStringData()),
37 ref Unsafe
.As
<char, byte>(ref strB
.GetRawStringData()),
38 ((nuint
)strA
.Length
) * 2);
41 [MethodImpl(MethodImplOptions
.AggressiveInlining
)]
42 private static int CompareOrdinalHelper(string strA
, int indexA
, int countA
, string strB
, int indexB
, int countB
)
44 Debug
.Assert(strA
!= null);
45 Debug
.Assert(strB
!= null);
46 Debug
.Assert(indexA
>= 0 && indexB
>= 0);
47 Debug
.Assert(countA
>= 0 && countB
>= 0);
48 Debug
.Assert(indexA
+ countA
<= strA
.Length
&& indexB
+ countB
<= strB
.Length
);
50 return SpanHelpers
.SequenceCompareTo(ref Unsafe
.Add(ref strA
.GetRawStringData(), indexA
), countA
, ref Unsafe
.Add(ref strB
.GetRawStringData(), indexB
), countB
);
53 private static bool EqualsOrdinalIgnoreCase(string strA
, string strB
)
55 Debug
.Assert(strA
.Length
== strB
.Length
);
57 return CompareInfo
.EqualsOrdinalIgnoreCase(ref strA
.GetRawStringData(), ref strB
.GetRawStringData(), strB
.Length
);
59 private static unsafe int CompareOrdinalHelper(string strA
, string strB
)
61 Debug
.Assert(strA
!= null);
62 Debug
.Assert(strB
!= null);
64 // NOTE: This may be subject to change if eliminating the check
65 // in the callers makes them small enough to be inlined
66 Debug
.Assert(strA
._firstChar
== strB
._firstChar
,
67 "For performance reasons, callers of this method should " +
68 "check/short-circuit beforehand if the first char is the same.");
70 int length
= Math
.Min(strA
.Length
, strB
.Length
);
72 fixed (char* ap
= &strA
._firstChar
) fixed (char* bp
= &strB
._firstChar
)
77 // Check if the second chars are different here
78 // The reason we check if _firstChar is different is because
79 // it's the most common case and allows us to avoid a method call
81 // The reason we check if the second char is different is because
82 // if the first two chars the same we can increment by 4 bytes,
83 // leaving us word-aligned on both 32-bit (12 bytes into the string)
84 // and 64-bit (16 bytes) platforms.
86 // For empty strings, the second char will be null due to padding.
87 // The start of the string is the type pointer + string length, which
88 // takes up 8 bytes on 32-bit, 12 on x64. For empty strings the null
89 // terminator immediately follows, leaving us with an object
90 // 10/14 bytes in size. Since everything needs to be a multiple
91 // of 4/8, this will get padded and zeroed out.
93 // For one-char strings the second char will be the null terminator.
95 // NOTE: If in the future there is a way to read the second char
96 // without pinning the string (e.g. System.Runtime.CompilerServices.Unsafe
97 // is exposed to mscorlib, or a future version of C# allows inline IL),
98 // then do that and short-circuit before the fixed.
100 if (*(a
+ 1) != *(b
+ 1)) goto DiffOffset1
;
102 // Since we know that the first two chars are the same,
103 // we can increment by 2 here and skip 4 bytes.
104 // This leaves us 8-byte aligned, which results
105 // on better perf for 64-bit platforms.
106 length
-= 2; a
+= 2; b
+= 2;
112 if (*(long*)a
!= *(long*)b
) goto DiffOffset0
;
113 if (*(long*)(a
+ 4) != *(long*)(b
+ 4)) goto DiffOffset4
;
114 if (*(long*)(a
+ 8) != *(long*)(b
+ 8)) goto DiffOffset8
;
115 length
-= 12; a
+= 12; b
+= 12;
120 if (*(int*)a
!= *(int*)b
) goto DiffOffset0
;
121 if (*(int*)(a
+ 2) != *(int*)(b
+ 2)) goto DiffOffset2
;
122 if (*(int*)(a
+ 4) != *(int*)(b
+ 4)) goto DiffOffset4
;
123 if (*(int*)(a
+ 6) != *(int*)(b
+ 6)) goto DiffOffset6
;
124 if (*(int*)(a
+ 8) != *(int*)(b
+ 8)) goto DiffOffset8
;
125 length
-= 10; a
+= 10; b
+= 10;
130 // go back to slower code path and do comparison on 4 bytes at a time.
131 // This depends on the fact that the String objects are
132 // always zero terminated and that the terminating zero is not included
133 // in the length. For odd string sizes, the last compare will include
134 // the zero terminator.
137 if (*(int*)a
!= *(int*)b
) goto DiffNextInt
;
143 // At this point, we have compared all the characters in at least one string.
144 // The longer string will be larger.
145 return strA
.Length
- strB
.Length
;
148 DiffOffset8: a
+= 4; b
+= 4;
149 DiffOffset4: a
+= 4; b
+= 4;
151 // Use jumps instead of falling through, since
152 // otherwise going to DiffOffset8 will involve
153 // 8 add instructions before getting to DiffNextInt
154 DiffOffset8: a
+= 8; b
+= 8; goto DiffOffset0
;
155 DiffOffset6: a
+= 6; b
+= 6; goto DiffOffset0
;
156 DiffOffset4: a
+= 2; b
+= 2;
157 DiffOffset2: a
+= 2; b
+= 2;
161 // If we reached here, we already see a difference in the unrolled loop above
163 if (*(int*)a
== *(int*)b
)
170 if (*a
!= *b
) return *a
- *b
;
173 Debug
.Assert(*(a
+ 1) != *(b
+ 1), "This char must be different if we reach here!");
174 return *(a
+ 1) - *(b
+ 1);
178 // Provides a culture-correct string comparison. StrA is compared to StrB
179 // to determine whether it is lexicographically less, equal, or greater, and then returns
180 // either a negative integer, 0, or a positive integer; respectively.
182 public static int Compare(string? strA
, string? strB
)
184 return Compare(strA
, strB
, StringComparison
.CurrentCulture
);
188 // Provides a culture-correct string comparison. strA is compared to strB
189 // to determine whether it is lexicographically less, equal, or greater, and then a
190 // negative integer, 0, or a positive integer is returned; respectively.
191 // The case-sensitive option is set by ignoreCase
193 public static int Compare(string? strA
, string? strB
, bool ignoreCase
)
195 var comparisonType
= ignoreCase
? StringComparison
.CurrentCultureIgnoreCase
: StringComparison
.CurrentCulture
;
196 return Compare(strA
, strB
, comparisonType
);
200 // Provides a more flexible function for string comparison. See StringComparison
201 // for meaning of different comparisonType.
202 public static int Compare(string? strA
, string? strB
, StringComparison comparisonType
)
204 if (object.ReferenceEquals(strA
, strB
))
206 CheckStringComparison(comparisonType
);
210 // They can't both be null at this point.
213 CheckStringComparison(comparisonType
);
218 CheckStringComparison(comparisonType
);
222 switch (comparisonType
)
224 case StringComparison
.CurrentCulture
:
225 case StringComparison
.CurrentCultureIgnoreCase
:
226 return CultureInfo
.CurrentCulture
.CompareInfo
.Compare(strA
, strB
, GetCaseCompareOfComparisonCulture(comparisonType
));
228 case StringComparison
.InvariantCulture
:
229 case StringComparison
.InvariantCultureIgnoreCase
:
230 return CompareInfo
.Invariant
.Compare(strA
, strB
, GetCaseCompareOfComparisonCulture(comparisonType
));
232 case StringComparison
.Ordinal
:
233 // Most common case: first character is different.
234 // Returns false for empty strings.
235 if (strA
._firstChar
!= strB
._firstChar
)
237 return strA
._firstChar
- strB
._firstChar
;
240 return CompareOrdinalHelper(strA
, strB
);
242 case StringComparison
.OrdinalIgnoreCase
:
243 return CompareInfo
.CompareOrdinalIgnoreCase(strA
, strB
);
246 throw new ArgumentException(SR
.NotSupported_StringComparison
, nameof(comparisonType
));
251 // Provides a culture-correct string comparison. strA is compared to strB
252 // to determine whether it is lexicographically less, equal, or greater, and then a
253 // negative integer, 0, or a positive integer is returned; respectively.
255 public static int Compare(string? strA
, string? strB
, CultureInfo
? culture
, CompareOptions options
)
257 CultureInfo compareCulture
= culture
?? CultureInfo
.CurrentCulture
;
258 return compareCulture
.CompareInfo
.Compare(strA
, strB
, options
);
263 // Provides a culture-correct string comparison. strA is compared to strB
264 // to determine whether it is lexicographically less, equal, or greater, and then a
265 // negative integer, 0, or a positive integer is returned; respectively.
266 // The case-sensitive option is set by ignoreCase, and the culture is set
269 public static int Compare(string? strA
, string? strB
, bool ignoreCase
, CultureInfo
? culture
)
271 var options
= ignoreCase
? CompareOptions
.IgnoreCase
: CompareOptions
.None
;
272 return Compare(strA
, strB
, culture
, options
);
275 // Determines whether two string regions match. The substring of strA beginning
276 // at indexA of given length is compared with the substring of strB
277 // beginning at indexB of the same length.
279 public static int Compare(string? strA
, int indexA
, string? strB
, int indexB
, int length
)
281 // NOTE: It's important we call the boolean overload, and not the StringComparison
282 // one. The two have some subtly different behavior (see notes in the former).
283 return Compare(strA
, indexA
, strB
, indexB
, length
, ignoreCase
: false);
286 // Determines whether two string regions match. The substring of strA beginning
287 // at indexA of given length is compared with the substring of strB
288 // beginning at indexB of the same length. Case sensitivity is determined by the ignoreCase boolean.
290 public static int Compare(string? strA
, int indexA
, string? strB
, int indexB
, int length
, bool ignoreCase
)
292 // Ideally we would just forward to the string.Compare overload that takes
293 // a StringComparison parameter, and just pass in CurrentCulture/CurrentCultureIgnoreCase.
294 // That function will return early if an optimization can be applied, e.g. if
295 // (object)strA == strB && indexA == indexB then it will return 0 straightaway.
296 // There are a couple of subtle behavior differences that prevent us from doing so
298 // - string.Compare(null, -1, null, -1, -1, StringComparison.CurrentCulture) works
299 // since that method also returns early for nulls before validation. It shouldn't
300 // for this overload.
301 // - Since we originally forwarded to CompareInfo.Compare for all of the argument
302 // validation logic, the ArgumentOutOfRangeExceptions thrown will contain different
304 // Therefore, we have to duplicate some of the logic here.
306 int lengthA
= length
;
307 int lengthB
= length
;
311 lengthA
= Math
.Min(lengthA
, strA
.Length
- indexA
);
316 lengthB
= Math
.Min(lengthB
, strB
.Length
- indexB
);
319 var options
= ignoreCase
? CompareOptions
.IgnoreCase
: CompareOptions
.None
;
320 return CultureInfo
.CurrentCulture
.CompareInfo
.Compare(strA
, indexA
, lengthA
, strB
, indexB
, lengthB
, options
);
323 // Determines whether two string regions match. The substring of strA beginning
324 // at indexA of length length is compared with the substring of strB
325 // beginning at indexB of the same length. Case sensitivity is determined by the ignoreCase boolean,
326 // and the culture is set by culture.
328 public static int Compare(string? strA
, int indexA
, string? strB
, int indexB
, int length
, bool ignoreCase
, CultureInfo
? culture
)
330 var options
= ignoreCase
? CompareOptions
.IgnoreCase
: CompareOptions
.None
;
331 return Compare(strA
, indexA
, strB
, indexB
, length
, culture
, options
);
335 // Determines whether two string regions match. The substring of strA beginning
336 // at indexA of length length is compared with the substring of strB
337 // beginning at indexB of the same length.
339 public static int Compare(string? strA
, int indexA
, string? strB
, int indexB
, int length
, CultureInfo
? culture
, CompareOptions options
)
341 CultureInfo compareCulture
= culture
?? CultureInfo
.CurrentCulture
;
342 int lengthA
= length
;
343 int lengthB
= length
;
347 lengthA
= Math
.Min(lengthA
, strA
.Length
- indexA
);
352 lengthB
= Math
.Min(lengthB
, strB
.Length
- indexB
);
355 return compareCulture
.CompareInfo
.Compare(strA
, indexA
, lengthA
, strB
, indexB
, lengthB
, options
);
358 public static int Compare(string? strA
, int indexA
, string? strB
, int indexB
, int length
, StringComparison comparisonType
)
360 CheckStringComparison(comparisonType
);
362 if (strA
== null || strB
== null)
365 if (object.ReferenceEquals(strA
, strB
))
371 return strA
== null ? -1 : 1;
376 throw new ArgumentOutOfRangeException(nameof(length
), SR
.ArgumentOutOfRange_NegativeLength
);
379 if (indexA
< 0 || indexB
< 0)
381 string paramName
= indexA
< 0 ? nameof(indexA
) : nameof(indexB
);
382 throw new ArgumentOutOfRangeException(paramName
, SR
.ArgumentOutOfRange_Index
);
385 if (strA
.Length
- indexA
< 0 || strB
.Length
- indexB
< 0)
387 string paramName
= strA
.Length
- indexA
< 0 ? nameof(indexA
) : nameof(indexB
);
388 throw new ArgumentOutOfRangeException(paramName
, SR
.ArgumentOutOfRange_Index
);
391 if (length
== 0 || (object.ReferenceEquals(strA
, strB
) && indexA
== indexB
))
396 int lengthA
= Math
.Min(length
, strA
.Length
- indexA
);
397 int lengthB
= Math
.Min(length
, strB
.Length
- indexB
);
399 switch (comparisonType
)
401 case StringComparison
.CurrentCulture
:
402 case StringComparison
.CurrentCultureIgnoreCase
:
403 return CultureInfo
.CurrentCulture
.CompareInfo
.Compare(strA
, indexA
, lengthA
, strB
, indexB
, lengthB
, GetCaseCompareOfComparisonCulture(comparisonType
));
405 case StringComparison
.InvariantCulture
:
406 case StringComparison
.InvariantCultureIgnoreCase
:
407 return CompareInfo
.Invariant
.Compare(strA
, indexA
, lengthA
, strB
, indexB
, lengthB
, GetCaseCompareOfComparisonCulture(comparisonType
));
409 case StringComparison
.Ordinal
:
410 return CompareOrdinalHelper(strA
, indexA
, lengthA
, strB
, indexB
, lengthB
);
413 Debug
.Assert(comparisonType
== StringComparison
.OrdinalIgnoreCase
); // CheckStringComparison validated these earlier
414 return CompareInfo
.CompareOrdinalIgnoreCase(strA
, indexA
, lengthA
, strB
, indexB
, lengthB
);
418 // Compares strA and strB using an ordinal (code-point) comparison.
420 public static int CompareOrdinal(string? strA
, string? strB
)
422 if (object.ReferenceEquals(strA
, strB
))
427 // They can't both be null at this point.
437 // Most common case, first character is different.
438 // This will return false for empty strings.
439 if (strA
._firstChar
!= strB
._firstChar
)
441 return strA
._firstChar
- strB
._firstChar
;
444 return CompareOrdinalHelper(strA
, strB
);
447 [MethodImpl(MethodImplOptions
.AggressiveInlining
)]
448 internal static int CompareOrdinal(ReadOnlySpan
<char> strA
, ReadOnlySpan
<char> strB
)
449 => SpanHelpers
.SequenceCompareTo(ref MemoryMarshal
.GetReference(strA
), strA
.Length
, ref MemoryMarshal
.GetReference(strB
), strB
.Length
);
451 // Compares strA and strB using an ordinal (code-point) comparison.
453 public static int CompareOrdinal(string? strA
, int indexA
, string? strB
, int indexB
, int length
)
455 if (strA
== null || strB
== null)
457 if (object.ReferenceEquals(strA
, strB
))
463 return strA
== null ? -1 : 1;
466 // COMPAT: Checking for nulls should become before the arguments are validated,
467 // but other optimizations which allow us to return early should come after.
471 throw new ArgumentOutOfRangeException(nameof(length
), SR
.ArgumentOutOfRange_NegativeCount
);
474 if (indexA
< 0 || indexB
< 0)
476 string paramName
= indexA
< 0 ? nameof(indexA
) : nameof(indexB
);
477 throw new ArgumentOutOfRangeException(paramName
, SR
.ArgumentOutOfRange_Index
);
480 int lengthA
= Math
.Min(length
, strA
.Length
- indexA
);
481 int lengthB
= Math
.Min(length
, strB
.Length
- indexB
);
483 if (lengthA
< 0 || lengthB
< 0)
485 string paramName
= lengthA
< 0 ? nameof(indexA
) : nameof(indexB
);
486 throw new ArgumentOutOfRangeException(paramName
, SR
.ArgumentOutOfRange_Index
);
489 if (length
== 0 || (object.ReferenceEquals(strA
, strB
) && indexA
== indexB
))
494 return CompareOrdinalHelper(strA
, indexA
, lengthA
, strB
, indexB
, lengthB
);
497 // Compares this String to another String (cast as object), returning an integer that
498 // indicates the relationship. This method returns a value less than 0 if this is less than value, 0
499 // if this is equal to value, or a value greater than 0 if this is greater than value.
501 public int CompareTo(object? value)
508 if (!(value is string other
))
510 throw new ArgumentException(SR
.Arg_MustBeString
);
513 return CompareTo(other
); // will call the string-based overload
516 // Determines the sorting relation of StrB to the current instance.
518 public int CompareTo(string? strB
)
520 return string.Compare(this, strB
, StringComparison
.CurrentCulture
);
523 // Determines whether a specified string is a suffix of the current instance.
525 // The case-sensitive and culture-sensitive option is set by options,
526 // and the default culture is used.
528 public bool EndsWith(string value)
530 return EndsWith(value, StringComparison
.CurrentCulture
);
533 public bool EndsWith(string value, StringComparison comparisonType
)
535 if ((object)value == null)
537 throw new ArgumentNullException(nameof(value));
540 if ((object)this == (object)value)
542 CheckStringComparison(comparisonType
);
546 if (value.Length
== 0)
548 CheckStringComparison(comparisonType
);
552 switch (comparisonType
)
554 case StringComparison
.CurrentCulture
:
555 case StringComparison
.CurrentCultureIgnoreCase
:
556 return CultureInfo
.CurrentCulture
.CompareInfo
.IsSuffix(this, value, GetCaseCompareOfComparisonCulture(comparisonType
));
558 case StringComparison
.InvariantCulture
:
559 case StringComparison
.InvariantCultureIgnoreCase
:
560 return CompareInfo
.Invariant
.IsSuffix(this, value, GetCaseCompareOfComparisonCulture(comparisonType
));
562 case StringComparison
.Ordinal
:
563 int offset
= this.Length
- value.Length
;
564 return (uint)offset
<= (uint)this.Length
&& this.AsSpan(offset
).SequenceEqual(value);
566 case StringComparison
.OrdinalIgnoreCase
:
567 return this.Length
< value.Length
? false : (CompareInfo
.CompareOrdinalIgnoreCase(this, this.Length
- value.Length
, value.Length
, value, 0, value.Length
) == 0);
570 throw new ArgumentException(SR
.NotSupported_StringComparison
, nameof(comparisonType
));
574 public bool EndsWith(string value, bool ignoreCase
, CultureInfo
? culture
)
578 throw new ArgumentNullException(nameof(value));
581 if ((object)this == (object)value)
586 CultureInfo referenceCulture
= culture
?? CultureInfo
.CurrentCulture
;
587 return referenceCulture
.CompareInfo
.IsSuffix(this, value, ignoreCase
? CompareOptions
.IgnoreCase
: CompareOptions
.None
);
590 public bool EndsWith(char value)
592 int lastPos
= Length
- 1;
593 return ((uint)lastPos
< (uint)Length
) && this[lastPos
] == value;
596 // Determines whether two strings match.
597 public override bool Equals(object? obj
)
599 if (object.ReferenceEquals(this, obj
))
602 if (!(obj
is string str
))
605 if (this.Length
!= str
.Length
)
608 return EqualsHelper(this, str
);
611 // Determines whether two strings match.
612 public bool Equals(string? value)
614 if (object.ReferenceEquals(this, value))
617 // NOTE: No need to worry about casting to object here.
618 // If either side of an == comparison between strings
619 // is null, Roslyn generates a simple ceq instruction
620 // instead of calling string.op_Equality.
624 if (this.Length
!= value.Length
)
627 return EqualsHelper(this, value);
630 public bool Equals(string? value, StringComparison comparisonType
)
632 if (object.ReferenceEquals(this, value))
634 CheckStringComparison(comparisonType
);
640 CheckStringComparison(comparisonType
);
644 switch (comparisonType
)
646 case StringComparison
.CurrentCulture
:
647 case StringComparison
.CurrentCultureIgnoreCase
:
648 return (CultureInfo
.CurrentCulture
.CompareInfo
.Compare(this, value, GetCaseCompareOfComparisonCulture(comparisonType
)) == 0);
650 case StringComparison
.InvariantCulture
:
651 case StringComparison
.InvariantCultureIgnoreCase
:
652 return (CompareInfo
.Invariant
.Compare(this, value, GetCaseCompareOfComparisonCulture(comparisonType
)) == 0);
654 case StringComparison
.Ordinal
:
655 if (this.Length
!= value.Length
)
657 return EqualsHelper(this, value);
659 case StringComparison
.OrdinalIgnoreCase
:
660 if (this.Length
!= value.Length
)
663 return EqualsOrdinalIgnoreCase(this, value);
666 throw new ArgumentException(SR
.NotSupported_StringComparison
, nameof(comparisonType
));
671 // Determines whether two Strings match.
672 public static bool Equals(string? a
, string? b
)
674 if (object.ReferenceEquals(a
,b
))
679 if (a
is null || b
is null || a
.Length
!= b
.Length
)
684 return EqualsHelper(a
, b
);
687 public static bool Equals(string? a
, string? b
, StringComparison comparisonType
)
689 if (object.ReferenceEquals(a
, b
))
691 CheckStringComparison(comparisonType
);
695 if (a
is null || b
is null)
697 CheckStringComparison(comparisonType
);
701 switch (comparisonType
)
703 case StringComparison
.CurrentCulture
:
704 case StringComparison
.CurrentCultureIgnoreCase
:
705 return (CultureInfo
.CurrentCulture
.CompareInfo
.Compare(a
, b
, GetCaseCompareOfComparisonCulture(comparisonType
)) == 0);
707 case StringComparison
.InvariantCulture
:
708 case StringComparison
.InvariantCultureIgnoreCase
:
709 return (CompareInfo
.Invariant
.Compare(a
, b
, GetCaseCompareOfComparisonCulture(comparisonType
)) == 0);
711 case StringComparison
.Ordinal
:
712 if (a
.Length
!= b
.Length
)
714 return EqualsHelper(a
, b
);
716 case StringComparison
.OrdinalIgnoreCase
:
717 if (a
.Length
!= b
.Length
)
720 return EqualsOrdinalIgnoreCase(a
, b
);
723 throw new ArgumentException(SR
.NotSupported_StringComparison
, nameof(comparisonType
));
727 public static bool operator ==(string? a
, string? b
)
729 return string.Equals(a
, b
);
732 public static bool operator !=(string? a
, string? b
)
734 return !string.Equals(a
, b
);
737 // Gets a hash code for this string. If strings A and B are such that A.Equals(B), then
738 // they will return the same hash code.
739 [MethodImpl(MethodImplOptions
.AggressiveInlining
)]
740 public override int GetHashCode()
742 ulong seed
= Marvin
.DefaultSeed
;
744 // Multiplication below will not overflow since going from positive Int32 to UInt32.
745 return Marvin
.ComputeHash32(ref Unsafe
.As
<char, byte>(ref _firstChar
), (uint)_stringLength
* 2 /* in bytes, not chars */, (uint)seed
, (uint)(seed
>> 32));
748 // Gets a hash code for this string and this comparison. If strings A and B and comparison C are such
749 // that string.Equals(A, B, C), then they will return the same hash code with this comparison C.
750 public int GetHashCode(StringComparison comparisonType
) => StringComparer
.FromComparison(comparisonType
).GetHashCode(this);
752 [MethodImpl(MethodImplOptions
.AggressiveInlining
)]
753 internal int GetHashCodeOrdinalIgnoreCase()
755 ulong seed
= Marvin
.DefaultSeed
;
756 return Marvin
.ComputeHash32OrdinalIgnoreCase(ref _firstChar
, _stringLength
/* in chars, not bytes */, (uint)seed
, (uint)(seed
>> 32));
759 // A span-based equivalent of String.GetHashCode(). Computes an ordinal hash code.
760 [MethodImpl(MethodImplOptions
.AggressiveInlining
)]
761 public static int GetHashCode(ReadOnlySpan
<char> value)
763 ulong seed
= Marvin
.DefaultSeed
;
765 // Multiplication below will not overflow since going from positive Int32 to UInt32.
766 return Marvin
.ComputeHash32(ref Unsafe
.As
<char, byte>(ref MemoryMarshal
.GetReference(value)), (uint)value.Length
* 2 /* in bytes, not chars */, (uint)seed
, (uint)(seed
>> 32));
769 // A span-based equivalent of String.GetHashCode(StringComparison). Uses the specified comparison type.
770 public static int GetHashCode(ReadOnlySpan
<char> value, StringComparison comparisonType
)
772 switch (comparisonType
)
774 case StringComparison
.CurrentCulture
:
775 case StringComparison
.CurrentCultureIgnoreCase
:
776 return CultureInfo
.CurrentCulture
.CompareInfo
.GetHashCode(value, GetCaseCompareOfComparisonCulture(comparisonType
));
778 case StringComparison
.InvariantCulture
:
779 case StringComparison
.InvariantCultureIgnoreCase
:
780 return CultureInfo
.InvariantCulture
.CompareInfo
.GetHashCode(value, GetCaseCompareOfComparisonCulture(comparisonType
));
782 case StringComparison
.Ordinal
:
783 return GetHashCode(value);
785 case StringComparison
.OrdinalIgnoreCase
:
786 return GetHashCodeOrdinalIgnoreCase(value);
789 ThrowHelper
.ThrowArgumentException(ExceptionResource
.NotSupported_StringComparison
, ExceptionArgument
.comparisonType
);
790 Debug
.Fail("Should not reach this point.");
795 [MethodImpl(MethodImplOptions
.AggressiveInlining
)]
796 internal static int GetHashCodeOrdinalIgnoreCase(ReadOnlySpan
<char> value)
798 ulong seed
= Marvin
.DefaultSeed
;
799 return Marvin
.ComputeHash32OrdinalIgnoreCase(ref MemoryMarshal
.GetReference(value), value.Length
/* in chars, not bytes */, (uint)seed
, (uint)(seed
>> 32));
802 // Use this if and only if 'Denial of Service' attacks are not a concern (i.e. never used for free-form user input),
803 // or are otherwise mitigated
804 internal unsafe int GetNonRandomizedHashCode()
806 fixed (char* src
= &_firstChar
)
808 Debug
.Assert(src
[this.Length
] == '\0', "src[this.Length] == '\\0'");
809 Debug
.Assert(((int)src
) % 4 == 0, "Managed string should start at 4 bytes boundary");
811 uint hash1
= (5381 << 16) + 5381;
814 uint* ptr
= (uint*)src
;
815 int length
= this.Length
;
820 // Where length is 4n-1 (e.g. 3,7,11,15,19) this additionally consumes the null terminator
821 hash1
= (BitOperations
.RotateLeft(hash1
, 5) + hash1
) ^ ptr
[0];
822 hash2
= (BitOperations
.RotateLeft(hash2
, 5) + hash2
) ^ ptr
[1];
828 // Where length is 4n-3 (e.g. 1,5,9,13,17) this additionally consumes the null terminator
829 hash2
= (BitOperations
.RotateLeft(hash2
, 5) + hash2
) ^ ptr
[0];
832 return (int)(hash1
+ (hash2
* 1566083941));
836 // Determines whether a specified string is a prefix of the current instance
838 public bool StartsWith(string value)
842 throw new ArgumentNullException(nameof(value));
844 return StartsWith(value, StringComparison
.CurrentCulture
);
847 public bool StartsWith(string value, StringComparison comparisonType
)
851 throw new ArgumentNullException(nameof(value));
854 if ((object)this == (object)value)
856 CheckStringComparison(comparisonType
);
860 if (value.Length
== 0)
862 CheckStringComparison(comparisonType
);
866 switch (comparisonType
)
868 case StringComparison
.CurrentCulture
:
869 case StringComparison
.CurrentCultureIgnoreCase
:
870 return CultureInfo
.CurrentCulture
.CompareInfo
.IsPrefix(this, value, GetCaseCompareOfComparisonCulture(comparisonType
));
872 case StringComparison
.InvariantCulture
:
873 case StringComparison
.InvariantCultureIgnoreCase
:
874 return CompareInfo
.Invariant
.IsPrefix(this, value, GetCaseCompareOfComparisonCulture(comparisonType
));
876 case StringComparison
.Ordinal
:
877 if (this.Length
< value.Length
|| _firstChar
!= value._firstChar
)
881 return (value.Length
== 1) ?
882 true : // First char is the same and thats all there is to compare
883 SpanHelpers
.SequenceEqual(
884 ref Unsafe
.As
<char, byte>(ref this.GetRawStringData()),
885 ref Unsafe
.As
<char, byte>(ref value.GetRawStringData()),
886 ((nuint
)value.Length
) * 2);
888 case StringComparison
.OrdinalIgnoreCase
:
889 if (this.Length
< value.Length
)
893 return CompareInfo
.EqualsOrdinalIgnoreCase(ref this.GetRawStringData(), ref value.GetRawStringData(), value.Length
);
896 throw new ArgumentException(SR
.NotSupported_StringComparison
, nameof(comparisonType
));
900 public bool StartsWith(string value, bool ignoreCase
, CultureInfo
? culture
)
904 throw new ArgumentNullException(nameof(value));
907 if ((object)this == (object)value)
912 CultureInfo referenceCulture
= culture
?? CultureInfo
.CurrentCulture
;
913 return referenceCulture
.CompareInfo
.IsPrefix(this, value, ignoreCase
? CompareOptions
.IgnoreCase
: CompareOptions
.None
);
916 public bool StartsWith(char value) => Length
!= 0 && _firstChar
== value;
918 internal static void CheckStringComparison(StringComparison comparisonType
)
920 // Single comparison to check if comparisonType is within [CurrentCulture .. OrdinalIgnoreCase]
921 if ((uint)comparisonType
> (uint)StringComparison
.OrdinalIgnoreCase
)
923 ThrowHelper
.ThrowArgumentException(ExceptionResource
.NotSupported_StringComparison
, ExceptionArgument
.comparisonType
);
927 internal static CompareOptions
GetCaseCompareOfComparisonCulture(StringComparison comparisonType
)
929 Debug
.Assert((uint)comparisonType
<= (uint)StringComparison
.OrdinalIgnoreCase
);
931 // Culture enums can be & with CompareOptions.IgnoreCase 0x01 to extract if IgnoreCase or CompareOptions.None 0x00
933 // CompareOptions.None 0x00
934 // CompareOptions.IgnoreCase 0x01
936 // StringComparison.CurrentCulture: 0x00
937 // StringComparison.InvariantCulture: 0x02
938 // StringComparison.Ordinal 0x04
940 // StringComparison.CurrentCultureIgnoreCase: 0x01
941 // StringComparison.InvariantCultureIgnoreCase: 0x03
942 // StringComparison.OrdinalIgnoreCase 0x05
944 return (CompareOptions
)((int)comparisonType
& (int)CompareOptions
.IgnoreCase
);