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]
Posted in C#

Leave a Reply

Your email address will not be published. Required fields are marked *