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
orToArray
)
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]