Fun times with string.join in C#

I had an interesting issue the other day at work regarding string.Join. I had overridden ToString in one of my classes to easily output values from the object, and this new ToString implementation was utilizing string.Join:

class Data
{
    public string One { get; set; }
    public string Two { get; set; }
    public DateTime Three{ get; set; }

    public override string ToString()
    {
        return string.Join(",", One, Two, Three);
    }
}

Everything was going swimmingly until I noticed I would randomly get an empty string returned from my call to ToString. After some debugging, I noticed that it happened only when the first item in the array was null, like so:

class Program
{
    static void Main()
    {
        var data = new Data
            {
                One = null,
                Two = "2",
                Three = DateTime.Now,
            };

        Console.WriteLine(data.ToString()); //Empty string written. Whaaaaat??!?
        Console.Read();
    }
}

So, obviously, my first thought was to go to the documentation and review what MSDN says about string.Join. I had assumed that there would be wording about it returning an empty string if one of the items is null, but I was very, very wrong:

If separator is null, an empty string (String.Empty) is used instead. If any element in value is null, an empty string is used instead.

I was baffled. If that’s the case, what was happening here? My call to Join worked perfectly when all items had values or even when any item but the first one was null. After some more unsuccessful debugging, I fired up DotPeek to review the source on string.Join. I originally looked at the Join(string separator, params string[] value) overload, and this is what I found:

public static string Join(string separator, params string[] value)
{
  if (value == null)
    throw new ArgumentNullException("value");
  else
    return string.Join(separator, value, 0, value.Length);
}

This overload only checks to ensure that value is not null. No help there. There is a lot of fun inside the Join(string separator, string[] value, int startIndex, int count) overload, but it didn’t end my search for answers:

public static unsafe string Join(string separator, string[] value, int startIndex, int count)
{
  if (value == null)
    throw new ArgumentNullException("value");
  if (startIndex < 0)
    throw new ArgumentOutOfRangeException("startIndex", Environment.GetResourceString("ArgumentOutOfRange_StartIndex"));
  if (count < 0)
    throw new ArgumentOutOfRangeException("count", Environment.GetResourceString("ArgumentOutOfRange_NegativeCount"));
  if (startIndex > value.Length - count)
    throw new ArgumentOutOfRangeException("startIndex", Environment.GetResourceString("ArgumentOutOfRange_IndexCountBuffer"));
  if (separator == null)
    separator = string.Empty;
  if (count == 0)
    return string.Empty;
  int num1 = 0;
  int num2 = startIndex + count - 1;
  for (int index = startIndex; index <= num2; ++index)
  {
    if (value[index] != null)
      num1 += value[index].Length;
  }
  int num3 = num1 + (count - 1) * separator.Length;
  if (num3 < 0 || num3 + 1 < 0)
    throw new OutOfMemoryException();
  if (num3 == 0)
    return string.Empty;
  string str = string.FastAllocateString(num3);
  fixed (char* buffer = &str.m_firstChar)
  {
    UnSafeCharBuffer unSafeCharBuffer = new UnSafeCharBuffer(buffer, num3);
    unSafeCharBuffer.AppendString(value[startIndex]);
    for (int index = startIndex + 1; index <= num2; ++index)
    {
      unSafeCharBuffer.AppendString(separator);
      unSafeCharBuffer.AppendString(value[index]);
    }
  }
  return str;
}

Join has an optimized overload for string arrays which is in use here. The call to unSafeCharBuffer.AppendString checks against null and will not doing anything if value is null. That doesn’t help either. I realized my mistake, though, when I remembered I’m passing in an object array due to my DateTime property. This changes everything! I immediately reviewed the implementation for that overload:

public static string Join(string separator, params object[] values)
{
  if (values == null)
    throw new ArgumentNullException("values");
  if (values.Length == 0 || values[0] == null)
    return string.Empty;
  if (separator == null)
    separator = string.Empty;
  StringBuilder sb = StringBuilderCache.Acquire(16);
  string str1 = values[0].ToString();
  if (str1 != null)
    sb.Append(str1);
  for (int index = 1; index < values.Length; ++index)
  {
    sb.Append(separator);
    if (values[index] != null)
    {
      string str2 = values[index].ToString();
      if (str2 != null)
        sb.Append(str2);
    }
  }
  return StringBuilderCache.GetStringAndRelease(sb);
}

Huzzah! The overload for Join that takes an object array checks the first item against null, and if it is null returns an empty string. I was very happy to get to the bottom of it, but was then confused that my findings conflicted with the documentation on MSDN. I re-referenced the docs and low and behold, inside the overload I was actually using was the sentence:

If separator is null or if any element of values other than the first element is null, an empty string (String.Empty) is used instead.

Moral of the story: Read the documentation correctly the first time.