Notes from C# Tips and Traps (Incomplete)

Following are the notes that I prepared while viewing course videos from C# Tips and Traps course from Jason Roberts. The code samples should be self-explanatory as they are supported by comments wherever needed. Most of them can compile successfully on LINQPad.

Part I

1.1. Customize debugger display values

  • Override ToString() method.
  • Use DebuggerDisplay attribute.
[DebuggerDisplay("{Name} ({Age} year(s))")] // "Sarah" (50 year(s))
class Person
{
	public string Name { get; set; }

	[DebuggerDisplay("{Age} year(s)"] // 50 year(s)
	public int AgeInYears { get; set; }

	public override string ToString()
	{
		return $"{Name} ({Age} year(s))"); // Sarah (50 year(s))
	}
}

1.2. Control the display of members in the debugger

  • Use DebuggerBrowsable attribute.
class Person
{
	public string Name { get; set; }

	[DebuggerBrowsable(DebuggerBrowsableState.Never)]
	public int AgeInYears { get; set; }

	[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
	public int FavoriteColor { get; set; }
}

1.3. The null-coalescing operator

string userLanguage = null;
string appLanguage = "English";

string language = userLanguage ?? appLanguage ?? "None"; // "English"

1.4. The dangers of virtual method calls from constructor

class BaseClass
{
	public BaseClass
	{
		int length = GetName().Length;
	}

	private virtual string GetName() => "Sarah";
}

class DerivedClass
{
	private override string GetName() => null;
}

new DerivedClass(); // throws NullReferenceException

1.5 The caller information attribute

private Caller()
{
	Callee("Hello");
}

private Callee(
	string message,
	[CallerFilePath] string filePath = null,
	[CallerMemberName] string memberName = null,
	[CallerLineNumber] int lineNumber = 0)
{
	// [C:\..\hello.cs:Caller:42]: Message: Hello
	Debug.WriteLine($"[{filePath}:{memberName}:{lineNumber}]: Message: {message}.");
}
  • Implementing INotifyPropertyChanged interface
class Person : INotifyPropertyChanged
{
	private string name;
	private int age;
	public string Name
	{
		get { return this.name; }
		set { this.name = value; OnPropertyChanged(); }
	}
	public int Age
	{
		get { return this.age; }
		set { this.age = value; OnPropertyChanged(); }
	}
	public event PropertyChangedEventHandler PropertyChanged;
	protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
	{
		PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
	}
}

1.6. Partial types and methods

// Auto-generated partial class.
partial class Person
{
	public string FirstName { get; set; }
	public string LastName { get; set; }

	public string GetFullName()
	{
		string fullName = $"{FirstName} {LastName}";

		// Following line does not get compiled if partial method is not implemented.
		GetFullNameImpl(ref fullName);

		return fullName;
	}

	// Partial bodyless method must have void return type and is implicity private in scope.
	partial void GetFullNameImpl(ref string fullName);
}

// Hand-authored class (stays unaffected even if sister class is regenerated)
partial class Person
{
	// Partial method implementation, though not required for compilation to succeed.
	partial void GetFullNameImpl(ref string fullName)
	{
		fullName = $"{LastName}, {FirstName}";
	}

	// Can define other methods.
	public int GetNumber() => 42;
}

Person p = new Person { FirstName: "Jatin", LastName: "Sanghvi" };
Debug.WriteLine(p.GetFullName()); // Sanghvi, Jatin

1.7. Runtime conversions with Convert.ChangeType

TTarget Convert<TTarget>(object initialValue)
{
	T convertedValue = Convert.ChangeType(initialValue, typeof (T));
}

Convert<int>("99"); // returns 99
Convert<double>("99"); // returns 99.0

1.8. Expose internal types and members to friend assemblies

  • Update AssemblyInfo.cs.
// Refer external assembly by name.
[assembly: InternalsVisibleTo("TestProject")]

// Or refer it by name and public key.
[assembly: InternalsVisibleTo("TestProject, PublicKey=xxxx")]

Part II

2.1. Simplifying string empty and null checking code

string nullString = null, emptyString = string.Empty, whitespaceString = " ", visibleString = "x"; 

string.IsNullOrEmpty(nullString); // true
string.IsNullOrEmpty(emptyString); // true
string.IsNullOrEmpty(whitespaceString); // false

string.IsNullOrWhitespace(whitespaceString); // true
string.IsNullOrWhitespace(visibleString); // false

2.2. Time zones and using DateTime.MinValue to represent null dates

DateTime date = DateTime.MinValue;
date == date.ToLocalTime(); // true for GMT and later time zones, false otherwise

2.3. Conditional compilation and emitting compiler warnings and errors

void Method()
{
#if DEBUG && RELEASE

// Produces build error.
#error Both DEBUG and RELEASE compilation symbols are defined.

#elif DEBUG
	Console.WriteLine("Debug");
#elif RELEASE
	Console.WriteLine("Release");
#else

// Produces build warning. 
#warning None of DEBUG or RELEASE compilation symbols are defined.

	Console.WriteLine("Other");
#endif
}

// Produces output based on available conditional compilation symbols.
Method();

2.4 Testing char Unicode validity

bool isValidCharacter(char character)
{
	// OtherNotAssigned is just one of many UnicodeCategory enum values.
	return char.GetUnicodeCategory(character) != UnicodeCategory.OtherNotAssigned;
}

isValidCharacter('x'); // true
isValidCharacter((char)888) // false

2.5. Changing the current thread’s culture at runtime

var australiaCultureInfo = CultureInfo.GetCultureInfo("en-AU");
Thead.CurrentThread.CurrentCulture = australiaCultureInfo;

Debug.WriteLine("i".ToUpper()); // I
Debug.WriteLine(23.45); // 23.45
Debug.WriteLine(DateTime.Now); // 30/09/2017 9:59:59 PM

var turkeyCultureInfo = CultureInfo.GetCultureInfo("tr-TR");
Thead.CurrentThread.CurrentCulture = turkeyCultureInfo;

Debug.WriteLine("i".ToUpper()); // İ
Debug.WriteLine(23.45); // 23,45
Debug.WriteLine(DateTime.Now); // 30.9.2017 21:59:59

2.6. Creating random numbers

// Following generates same sequence, since r1 and r2 objects were instantiated approximately during same time and hence
// use same seed value.
Random r1 = new Random(), r2 = new Random();
Debug.WriteLine($"{r1.Next()}, {r1.Next()}, {r1.Next()}");
Debug.WriteLine($"{r2.Next()}, {r2.Next()}, {r2.Next()}");

// Following generates all different random numbers.
Random r = new Random();
Debug.WriteLine($"{r.Next()}, {r.Next()}, {r.Next()}");
Debug.WriteLine($"{r.Next()}, {r.Next()}, {r.Next()}");

// Sample code to illustrate random number generation for high security applications.
RandomNumberGenerator provider = RandomNumberGenerator.Create();

byte[] randomBytes = new byte[4];
provider.GetBytes(randomBytes);

int randomInt = BitConverter.ToInt32(randomBytes, 0);
Debug.WriteLine(randomInt);

2.7. Using Tuples to reduce code

var tupleOneElement = new Tuple<int>(1);
var tupleTwoElements = new Tuple<int, string>(1, "hello");

// ArgumentException if the last element of an eight element Tuple is not a Tuple.
var tupleEightElements = new Tuple<int, int, int, int, int, int, int, Tuple<int>>(1, 2, 3, 4, 5, 6, 7, new Tuple<int>(8));

// Using static generic method - Create
var tuple1 = Tuple.Create(2, "howdy");
var tuple2 = Tuple.Create(11, Tuple.Create(2.1, "22"), DateTime.Parse("2017-12-31"));

// Accessing Tuple properties.
Debug.WriteLine(tuple1.Item2.Item1); // 2.1

// Tuples are immutable. Tuple items are read only and cannot be assigned to.
// tuple2.Item1 = 42;

// Comparing Tuples.
var tuple2 = Tuple.Create(11, Tuple.Create(2.1, "22"), DateTime.Parse("2017-12-31"));
Debug.WriteLine(tuple1 == tuple2); // False (Reference equality)
Debug.WriteLine(tuple1.Equals(tuple2)); // True (Value comparison)

// Using Tuples to return multiple values.
Tuple<string, int> GetNameAndScore()
{
	return new Tuple<string, int>("Sarah", 90);
}

var nameAndScore = GetNameAndScore();
Debug.WriteLine($"Name: {nameAndScore.Item1}, Score: {nameAndScore.Item2}.");

2.8. Forcing reference equality comparisons

Uri uri1 = new Uri("https://pluralsight.com");
Uri uri2 = new Uri("https://pluralsight.com");

Debug.WriteLine(object.Equals(uri1, uri2)); // True
Debug.WriteLine(uri1.Equals(uri2)); // True
Debug.WriteLine(uri1 == uri2); // True
Debug.WriteLine(object.ReferenceEquals(uri1, uri2)); // False

uri2 = uri1;
Debug.WriteLine(object.ReferenceEquals(uri1, uri2)); // True

2.9. Don’t change an object’s hashcode after adding to a dictionary

class PersonId
{
	public int Id { get; set; }
	public override int GetHashCode() => Id.GetHashCode();
}

void Main()
{
	Dictionary<PersonId, string> personName = new Dictionary<PersonId, string>();

	PersonId sarah = new PersonId { Id = 1 };
	PersonId john =  new PersonId { Id = 2 };

	personName.Add(sarah, "Sarah");
	personName.Add(john, "John");

	Debug.WriteLine(personName[john]); // John

	// Following causes KeyNotFoundException, since Dictionary lookup will fail with new john's hash code.
	john.Id = 3;
	Debug.WriteLine(personName[john]);
}

2.10. Creating and using combinable enums

// Flag based enums should have plural names.
[Flags]
enum Borders
{
	None = 0,
	Top = 1,
	Right = 2,
	Bottom = 4,
	Left = 8
}

void Main()
{
	// Creating flag combination.
	Borders topRight = Borders.Top | Borders.Right;

	// Combining flag combinations.
	Borders bottomLeft = Borders.Bottom | Borders.Left;
	Borders all = topRight | bottomLeft;

	// Evaluating flag within combination.
	Debug.WriteLine((topRight & Borders.Right) != Borders.None); // True
	Debug.WriteLine(topRight.HasFlag(Borders.Right)); // True

	Debug.WriteLine((topRight | ~Borders.Left) != topRight); // True
	Debug.WriteLine(!topRight.HasFlag(Borders.Left)); // True
}

Part III

3.1. Conditional formatting for positive, negative, and zero numbers

string formatString = "pos<#.##>;neg(#.##);zero";

Debug.WriteLine((23.555).ToString(formatString)); // pos<23.56>
Debug.WriteLine((-23.555).ToString(formatString)); // neg(23.56)
Debug.WriteLine((0.0).ToString(formatString)); // zero

3.2. Marking code as obsolete

[Obsolete]
class ObsoleteClass { }

[Obsolete("Use NewClass from now on")]
class ObsoleteClassWithWarningMessage { }

class NewClass
{
	[Obsolete("Use NewProperty from now on")]
	public string ObsoleteProperty { get; set; }

	public string NewProperty { get; set; }
}

[Obsolete("This class can no longer be used", true)]
class ObsoleteClassWithErrorMessage { }

void Main()
{
	// Generates compilation warning: 'ObsoleteClass' is obsolete.
	new ObsoleteClass();

	// Generates warning: 'ObsoleteClassWithWarningMessage' is obsolete: Use NewClass from now on.
	new ObsoleteClassWithWarningMessage();

	// Generates warning: 'NewClass.ObsoleteProperty' is obsolete: Use NewProperty from now on.
	new NewClass().ObsoleteProperty = "PropertyValue";

	// Generates error: 'ObsoleteClassWithErrorMessage' is obsolete: This class can no longer be used.
	new ObsoleteClassWithErrorMessage();
}

3.3. Avoiding re-evaluation of LINQ queries

  • Before
List<int> numbers = new List<int>() { 1, 2 };
Random random = new Random();

var sequence = numbers.Select(n => new { Number = n, Random = random.Next() });

Debug.WriteLine(sequence.First());
Debug.WriteLine(sequence.First());

// Output
// { Number = 1, Random = 1822957719 }
// { Number = 1, Random = 519386891 }
  • After ( ToList or ToArray )
List<int> numbers = new List<int>() { 1, 2 };
Random random = new Random();

var sequence = numbers.Select(n => new { Number = n, Random = random.Next() }).ToList();

Debug.WriteLine(sequence.First());
Debug.WriteLine(sequence.First());

// Output
// { Number = 1, Random = 209052729 }
// { Number = 1, Random = 209052729 }
  • After ( ToDictionary )
List<int> numbers = new List<int>() { 1, 2 };
Random random = new Random();

var sequence = numbers.ToDictionary(n => n, n => random.Next());

Debug.WriteLine(sequence.First());
Debug.WriteLine(sequence.First());

// Output
// [1, 409999800]
// [1, 409999800]

LINQ Query Expression with Multiple Generators

Today, I went through the ‘LINQ’ section in C# 7.0 Pocket Reference. The section contains a sub-section on ‘Multiple Generators’ that illustrates how the compiler translates the query expression with multiple generators into a call to SelectMany LINQ query operator. An example from the book would make things clearer:

Query Expression

int[] numbers = { 1, 2, 3 };
string[] letters = { "a", "b" };

IEnumerable<string> query =
  from n in numbers
  from l in letters
  select n.ToString() + 1;

Compiler emitted code

IEnumerable<string> query =
  numbers.SelectMany(
    n => letters,
    (n, l) => select n.ToString() + 1));

In case you are encountering this particular overload of SelectMany operator for first time, it is basically implemented as an extension method that internally contains two foreach loops. The outer loop iterates through the sequence on which the extension method is called. The inner loop iterates through the sequence returned by lambda expression this is passed as SelectMany‘s first argument. SelectMany then calls lambda expression in second argument for each element-pair in outer and inner sequences.

Here is how the method is implemented:

private static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>( 
  IEnumerable<TSource> source, 
  Func<TSource, IEnumerable<TCollection>> collectionSelector, 
  Func<TSource, TCollection, TResult> resultSelector) 
{ 
  foreach (TSource item in source) 
  { 
    foreach (TCollection collectionItem in collectionSelector(item)) 
    { 
      yield return resultSelector(item, collectionItem); 
    } 
  } 
}

Back to the compiler emitted code, the first lambda expression (copied to collectionSelector  delegate instance) basically ignored its input parameter n and always output the same sequence letters. Hence, in effect we get a Cartesian product of the two sequences. Just for sake of clarity, if the SelectMany query operator did not exist, below would be the non-generic implementation to achieve same effect.

foreach (int item in numbers) 
{ 
  foreach (string collectionItem in letters) 
  { 
    yield return n.ToString() + l;
  } 
}

I wondered what the compiler translates the query expression into if there are three generators present in the query expression as below:

IEnumerable<string> increasings =
  from a in new[] { 1, 2, 3, 4 }
  from b in new[] { 1, 2, 3, 4 }
  from c in new[] { 1, 2, 3, 4 }
  where a < b && b < c
  select a + " " + b + " " + c;
  
increasings.ToList().ForEach(Console.WriteLine);

/*
  Result
 
  1 2 3
  1 2 4
  1 3 4
  2 3 4
*/

I compiled the program and inspected the assembly with help of ILSpy, I could see below code generated by compiler for the above method body:

Private internal class named <>c

private sealed class <>c
{
  public static readonly LinqTest.<>c <>9 = new LinqTest.<>c();

  public static Func<int, IEnumerable<int>> <>9__0_0;

  public static Func<int, int, <>f__AnonymousType0<int, int>> <>9__0_1;

  public static Func<<>f__AnonymousType0<int, int>, IEnumerable<int>> <>9__0_2;

  public static Func<<>f__AnonymousType0<int, int>, int, <>f__AnonymousType1<<>f__AnonymousType0<int, int>, int>> <>9__0_3;

  public static Func<<>f__AnonymousType1<<>f__AnonymousType0<int, int>, int>, bool> <>9__0_4;

  public static Func<<>f__AnonymousType1<<>f__AnonymousType0<int, int>, int>, string> <>9__0_5;

  internal IEnumerable<int> <Main>b__0_0(int a)
  {
    return new int[]
    {
      1,
      2,
      3,
      4
    };
  }

  internal <>f__AnonymousType0<int, int> <Main>b__0_1(int a, int b)
  {
    return new
    {
      a,
      b
    };
  }

  internal IEnumerable<int> <Main>b__0_2(<>f__AnonymousType0<int, int> <>h__TransparentIdentifier0)
  {
    return new int[]
    {
      1,
      2,
      3,
      4
    };
  }

  internal <>f__AnonymousType1<<>f__AnonymousType0<int, int>, int> <Main>b__0_3(<>f__AnonymousType0<int, int> <>h__TransparentIdentifier0, int c)
  {
    return new
    {
      <>h__TransparentIdentifier0,
      c
    };
  }

  internal bool <Main>b__0_4(<>f__AnonymousType1<<>f__AnonymousType0<int, int>, int> <>h__TransparentIdentifier1)
  {
    return <>h__TransparentIdentifier1.<>h__TransparentIdentifier0.a < <>h__TransparentIdentifier1.<>h__TransparentIdentifier0.b && <>h__TransparentIdentifier1.<>h__TransparentIdentifier0.b < <>h__TransparentIdentifier1.c;
  }

  internal string <Main>b__0_5(<>f__AnonymousType1<<>f__AnonymousType0<int, int>, int> <>h__TransparentIdentifier1)
  {
    return string.Concat(new object[]
    {
      <>h__TransparentIdentifier1.<>h__TransparentIdentifier0.a,
      " ",
      <>h__TransparentIdentifier1.<>h__TransparentIdentifier0.b,
      " ",
      <>h__TransparentIdentifier1.c
    });
  }
}

That seems too cryptic but if you see carefully, the member variables within the class can be grouped into three sets:

  1. Six methods named from <Main>b__0_0 to <Main>b__0_5 required while translating the query expression to method calls.
  2. Six Func delegate instances named from <>9__0_0  to <>9__0_5 corresponding to each method.
  3. Public static member named <>9 pointing to the containing class instance.

In our case a and b are outer generator variables and c is inner variable. If outer variables are referenced within the query expression, the compiler needs to ensure that both inner and outer variables are available and hence needs to generate anonymous types that contain all values. That is why we can see two anonymous types in the decompiled code above viz. <>f__AnonymousType0<int, int> and <>f__AnonymousType1<<>f__AnonymousType0<int, int>, int>. It was a kind of revelation to me to find that generic class types are used to for anonymous types in same way how Funcs and Actions are used as delegates for anonymous methods.

Compiler emitted method body

IEnumerable<int> expr_07 = new int[]
{
  1,
  2,
  3,
  4
};
Func<int, IEnumerable<int>> arg_50_1;
if ((arg_50_1 = LinqTest.<>c.<>9__0_0) == null)
{
  arg_50_1 = (LinqTest.<>c.<>9__0_0 = new Func<int, IEnumerable<int>>(LinqTest.<>c.<>9.<Main>b__0_0));
}
var arg_50_2;
if ((arg_50_2 = LinqTest.<>c.<>9__0_1) == null)
{
  arg_50_2 = (LinqTest.<>c.<>9__0_1 = new Func<int, int, <>f__AnonymousType0<int, int>>(LinqTest.<>c.<>9.<Main>b__0_1));
}
var arg_93_0 = expr_07.SelectMany(arg_50_1, arg_50_2);
var arg_93_1;
if ((arg_93_1 = LinqTest.<>c.<>9__0_2) == null)
{
  arg_93_1 = (LinqTest.<>c.<>9__0_2 = new Func<<>f__AnonymousType0<int, int>, IEnumerable<int>>(LinqTest.<>c.<>9.<Main>b__0_2));
}
var arg_93_2;
if ((arg_93_2 = LinqTest.<>c.<>9__0_3) == null)
{
  arg_93_2 = (LinqTest.<>c.<>9__0_3 = new Func<<>f__AnonymousType0<int, int>, int, <>f__AnonymousType1<<>f__AnonymousType0<int, int>, int>>(LinqTest.<>c.<>9.<Main>b__0_3));
}
var arg_B7_0 = arg_93_0.SelectMany(arg_93_1, arg_93_2);
var arg_B7_1;
if ((arg_B7_1 = LinqTest.<>c.<>9__0_4) == null)
{
  arg_B7_1 = (LinqTest.<>c.<>9__0_4 = new Func<<>f__AnonymousType1<<>f__AnonymousType0<int, int>, int>, bool>(LinqTest.<>c.<>9.<Main>b__0_4));
}
var arg_DB_0 = arg_B7_0.Where(arg_B7_1);
var arg_DB_1;
if ((arg_DB_1 = LinqTest.<>c.<>9__0_5) == null)
{
  arg_DB_1 = (LinqTest.<>c.<>9__0_5 = new Func<<>f__AnonymousType1<<>f__AnonymousType0<int, int>, int>, string>(LinqTest.<>c.<>9.<Main>b__0_5));
}
IEnumerable<string> increasings = arg_DB_0.Select(arg_DB_1);
increasings.ToList<string>().ForEach(new Action<string>(Console.WriteLine));

Again, the code seems difficult to comprehend but we have six local variables that get assigned the six delegate instances defined in class <>c. If they turn out to be null, both local and class member variables are assigned the methods defined in the same class. It would help if someone can comment on why is it required to lazily initialize the class member delegate instances.

Anyhow, I tried to trim away the parts that are not required to understand the translation by removing null checks and replacing the generic types for anonymous classes to non-generic named types. Here is what I was left with in the end:

class AB { public int a; public int b; };
class ABC { public AB ab; public int c; };

void Main()
{
  IEnumerable<int> aList = new int[] { 1, 2, 3, 4 };
  
  IEnumerable<AB> abList = aList.SelectMany(
    a => new int[] { 1, 2, 3, 4 },
    (a, b) => new AB { a = a, b = b });
  
  IEnumerable<ABC> abcList = abList.SelectMany(
    ab => new int[] { 1, 2, 3, 4 },
    (ab, c) => new ABC { ab = ab, c = c });
  
  IEnumerable<string> increasings = abcList
    .Where(abc => abc.ab.a < abc.ab.b && abc.ab.b < abc.c)
    .Select(abc => abc.ab.a + " " + abc.ab.b + " " + abc.c);

  increasings.ToList().ForEach(Console.WriteLine);
}

So everything made sense in the end. The compiler keeps creating anonymous classes nesting previous anonymous/named type into current one until all generators are taken cared of. The outer the variable is in the list of generators, the deeper it becomes the member of the final anonymous type.

To conclude, I have always preferred fluent query syntax over query expression and have managed to successfully avoid the latter in my projects until date. Still if in case, a requirement similar to above arrives, the non-query way would be far too complicated and error prone and will need to be dropped in favor of query expression.