Skip to content
This repository was archived by the owner on Nov 1, 2020. It is now read-only.
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 200 additions & 59 deletions src/System.Private.CoreLib/src/System/String.Manipulation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -470,34 +470,139 @@ public String Insert(int startIndex, String value)
return result;
}

public static string Join(char separator, params string[] value)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}

return Join(separator, value, 0, value.Length);
}

public unsafe static string Join(char separator, params object[] values)
{
// Defer argument validation to the internal function
return JoinCore(&separator, 1, values);
}

public unsafe static string Join<T>(char separator, IEnumerable<T> values)
{
// Defer argument validation to the internal function
return JoinCore(&separator, 1, values);
}

public unsafe static string Join(char separator, string[] value, int startIndex, int count)
{
// Defer argument validation to the internal function
return JoinCore(&separator, 1, value, startIndex, count);
}

// Joins an array of strings together as one string with a separator between each original string.
//
public static String Join(String separator, params String[] value)
public static string Join(string separator, params string[] value)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
return Join(separator, value, 0, value.Length);
}

public static string Join(string separator, params object[] values)
public unsafe static string Join(string separator, params object[] values)
{
separator = separator ?? string.Empty;
fixed (char* pSeparator = &separator._firstChar)
{
// Defer argument validation to the internal function
return JoinCore(pSeparator, separator.Length, values);
}
}

public unsafe static string Join<T>(string separator, IEnumerable<T> values)
{
separator = separator ?? string.Empty;
fixed (char* pSeparator = &separator._firstChar)
{
// Defer argument validation to the internal function
return JoinCore(pSeparator, separator.Length, values);
}
}

public static string Join(string separator, IEnumerable<string> values)
{
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}

using (IEnumerator<string> en = values.GetEnumerator())
{
if (!en.MoveNext())
{
return string.Empty;
}

string firstValue = en.Current;

if (!en.MoveNext())
{
// Only one value available
return firstValue ?? string.Empty;
}

// Null separator and values are handled by the StringBuilder
StringBuilder result = StringBuilderCache.Acquire();
result.Append(firstValue);

do
{
result.Append(separator);
result.Append(en.Current);
}
while (en.MoveNext());

return StringBuilderCache.GetStringAndRelease(result);
}
}

// Joins an array of strings together as one string with a separator between each original string.
//
public unsafe static string Join(string separator, string[] value, int startIndex, int count)
{
separator = separator ?? string.Empty;
fixed (char* pSeparator = &separator._firstChar)
{
// Defer argument validation to the internal function
return JoinCore(pSeparator, separator.Length, value, startIndex, count);
}
}

private unsafe static string JoinCore(char* separator, int separatorLength, object[] values)
{
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}

if (values.Length == 0)
{
return string.Empty;
}

string firstString = values[0]?.ToString();

if (values.Length == 1)
{
return firstString ?? string.Empty;
}

StringBuilder result = StringBuilderCache.Acquire();
result.Append(firstString);

for (int i = 1; i < values.Length; i++)
{
result.Append(separator);
result.Append(separator, separatorLength);
object value = values[i];
if (value != null)
{
Expand All @@ -508,16 +613,20 @@ public static string Join(string separator, params object[] values)
return StringBuilderCache.GetStringAndRelease(result);
}

public static String Join<T>(String separator, IEnumerable<T> values)
private unsafe static string JoinCore<T>(char* separator, int separatorLength, IEnumerable<T> values)
{
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}

using (IEnumerator<T> en = values.GetEnumerator())
{
if (!en.MoveNext())
{
return string.Empty;

}

// We called MoveNext once, so this will be the first item
T currentValue = en.Current;

Expand All @@ -544,7 +653,7 @@ public static String Join<T>(String separator, IEnumerable<T> values)
{
currentValue = en.Current;

result.Append(separator);
result.Append(separator, separatorLength);
if (currentValue != null)
{
result.Append(currentValue.ToString());
Expand All @@ -556,81 +665,113 @@ public static String Join<T>(String separator, IEnumerable<T> values)
}
}

public static String Join(String separator, IEnumerable<String> values)
private unsafe static string JoinCore(char* separator, int separatorLength, string[] value, int startIndex, int count)
{
if (values == null)
throw new ArgumentNullException(nameof(values));

using (IEnumerator<String> en = values.GetEnumerator())
{
if (!en.MoveNext())
return String.Empty;

String firstValue = en.Current;

if (!en.MoveNext())
{
// Only one value available
return firstValue ?? String.Empty;
}

// Null separator and values are handled by the StringBuilder
StringBuilder result = StringBuilderCache.Acquire();
result.Append(firstValue);
// If the separator is null, it is converted to an empty string before entering this function.
// Even for empty strings, fixed should never return null (it should return a pointer to a null char).
Debug.Assert(separator != null);
Debug.Assert(separatorLength >= 0);

do
{
result.Append(separator);
result.Append(en.Current);
} while (en.MoveNext());
return StringBuilderCache.GetStringAndRelease(result);
}
}

// Joins an array of strings together as one string with a separator between each original string.
//
public unsafe static String Join(String separator, String[] value, int startIndex, int count)
{
//Range check the array
if (value == null)
{
throw new ArgumentNullException(nameof(value));

}
if (startIndex < 0)
{
throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_StartIndex);
}
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_NegativeCount);

}
if (startIndex > value.Length - count)
{
throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_IndexCountBuffer);

//Treat null as empty string.
if (separator == null)
}

if (count <= 1)
{
separator = String.Empty;
return count == 0 ?
string.Empty :
value[startIndex] ?? string.Empty;
}

//If count is 0, that skews a whole bunch of the calculations below, so just special case that.
if (count == 0)
long totalSeparatorsLength = (long)(count - 1) * separatorLength;
if (totalSeparatorsLength > int.MaxValue)
{
return String.Empty;
throw new OutOfMemoryException();
}
int totalLength = (int)totalSeparatorsLength;

if (count == 1)
// Calculate the length of the resultant string so we know how much space to allocate.
for (int i = startIndex, end = startIndex + count; i < end; i++)
{
return value[startIndex] ?? String.Empty;
string currentValue = value[i];
if (currentValue != null)
{
totalLength += currentValue.Length;
if (totalLength < 0) // Check for overflow
{
throw new OutOfMemoryException();
}
}
}

int endIndex = startIndex + count - 1;
StringBuilder result = StringBuilderCache.Acquire();
// Append the first string first and then append each following string prefixed by the separator.
result.Append(value[startIndex]);
for (int stringToJoinIndex = startIndex + 1; stringToJoinIndex <= endIndex; stringToJoinIndex++)
// Copy each of the strings into the resultant buffer, interleaving with the separator.
string result = FastAllocateString(totalLength);
int copiedLength = 0;

for (int i = startIndex, end = startIndex + count; i < end; i++)
{
result.Append(separator);
result.Append(value[stringToJoinIndex]);
// It's possible that another thread may have mutated the input array
// such that our second read of an index will not be the same string
// we got during the first read.

// We range check again to avoid buffer overflows if this happens.

string currentValue = value[i];
if (currentValue != null)
{
int valueLen = currentValue.Length;
if (valueLen > totalLength - copiedLength)
{
copiedLength = -1;
break;
}

// Fill in the value.
FillStringChecked(result, copiedLength, currentValue);
copiedLength += valueLen;
}

if (i < end - 1)
{
// Fill in the separator.
fixed (char* pResult = &result._firstChar)
{
// If we are called from the char-based overload, we will not
// want to call MemoryCopy each time we fill in the separator. So
// specialize for 1-length separators.
if (separatorLength == 1)
{
pResult[copiedLength] = *separator;
}
else
{
wstrcpy(pResult + copiedLength, separator, separatorLength);
}
}
copiedLength += separatorLength;
}
}

return StringBuilderCache.GetStringAndRelease(result);
// If we copied exactly the right amount, return the new string. Otherwise,
// something changed concurrently to mutate the input array: fall back to
// doing the concatenation again, but this time with a defensive copy. This
// fall back should be extremely rare.
return copiedLength == totalLength ?
result :
JoinCore(separator, separatorLength, (string[])value.Clone(), startIndex, count);
}

public String PadLeft(int totalWidth)
Expand Down