- Declaration, Initialization & Assignment
- Control Flow
- Expressions
- Function
- OOP
- String
- Numeric
- Collection
- Others
- Implicit typing
var
: for long named type
// 👎 longhand
AReallyReallyLooooongClass instance = new AReallyReallyLooooongClass();
// 👍 shorthand
var instance = new AReallyReallyLooooongClass();
- Declare nullable type w/
T?
// 👎 longhand
Nullable<int> num = null;
// 👍 shorthand
int? num = null;
- Init object using target-typed new expressions
new()
[C#9]
// 👎 longhand
ExampleClass instance = new ExampleClass();
// 👍 shorthand
ExampleClass instance = new();
- Prefer object initializer syntax than overloading constructors or invoking multiples setters [Reference]
// 👎 non-compliant
User user1 = new();
user1.Name = "User 1";
user1.Age = 18;
// 👍 preference
User user1 = new {Name: "User 1"; Age: 18};
- Copy then modify record instance using with expression [C#9]
// 👉 given
User user1 = new {Name: "User 1"; Age: 18};
// 👎 longhand
User user2 = user1;
user2.Name = "User 2";
// 👍 shorthand
User user2 = user1 with {Name = "User 2");
- Init collection using collection initializer syntax
// 👎 longhand
List<string> users = new();
users.Add("User 1");
users.Add("User 2");
// 👍 shorthand
List<string> users = new {"User 1", "User 2");
- Swap two variables using tuple
// 👎 longhand
var temp = a;
a = b;
b = temp;
// 👍 shorthand
(a, b) = (b, a);
- Switch expressions [C#8]
// 👎 longhand
switch (DateTime.Now.DayOfWeek)
{
case DayOfWeek.Monday:
return "Not Weekend";
case DayOfWeek.Tuesday:
return "Not Weekend";
case DayOfWeek.Wednesday:
return "Not Weekend";
case DayOfWeek.Thursday:
return "Not Weekend";
case DayOfWeek.Friday:
return "Not Weekend";
case DayOfWeek.Saturday:
return "Weekend";
case DayOfWeek.Sunday:
return "Weekend";
default:
throw new ArgumentOutOfRangeException();
}
// 👍 shorthand
DateTime.Now.DayOfWeek switch
{
DayOfWeek.Monday => "Not Weekend",
DayOfWeek.Tuesday => "Not Weekend",
DayOfWeek.Wednesday => "Not Weekend",
DayOfWeek.Thursday => "Not Weekend",
DayOfWeek.Friday => "Not Weekend",
DayOfWeek.Saturday => "Weekend",
DayOfWeek.Sunday => "Weekend",
_ => throw new ArgumentOutOfRangeException()
}
// 👍👍 shorthand with pattern (not)
DateTime.Now.DayOfWeek switch
{
not (DayOfWeek.Saturday or DayOfWeek.Sunday) => "Not Weekend",
DayOfWeek.Saturday or DayOfWeek.Sunday => "Weekend",
_ => throw new ArgumentOutOfRangeException()
}
- Flatten continuous loops:
// 👎 non-compliant
foreach (var assembly in assemblies){
foreach (var type in assembly.GetTypes())
...
}
}
// 👍 preference
foreach (var assembly in assemblies)
foreach (var type in assembly.GetTypes()){
...
}
Used with control flow (if expression, switch case expression), = expression, return expression
==, !=, &&, ||=> pattern matchingis
& pattern combinatorsnot, and, or
[C#7]
Check type:
if (a is int)
Check null:
// 👉 given
User? user = null;
// 👎 non-compliant
if(!user.HasValue)
if(user == null)
// 👍 preference
if(user is null)
- Check equality for nullable objects using
Object.Equals()
// 👎 longhand
if ((user1 == user2) || ((user1 != null and user2 != null) and user1.Equals(user2)))
// 👍 shorthand
if(Object.Equals(user1, user2))
- Use
Equals() & OrdinalIgnoreCase
to compare strings regardless of case
✔️ Pros: removes the additional string allocation overhead
// 👎 non-compliant
str1.ToUpper() == str2.ToUpper()
// 👍 preference
str1.Equals(str2, StringComparison.OrdinalIgnoreCase)
Class/Struct=> Record: for DTO [C#9]
✔️ Pros: reference type, immutable by default
- Automatic properties
class User {
// 👎 longhand
private string _name;
public string Name {
get {return _name}
set (_name = value}
}
// 👍 shorthand
public string Name {get; set}
}
set accessor=> init accesor: for immutable properties in class/struct/record [C#9]
class User {
public string Name {get; set};
public int Age {get; init};
}
User user = new {Name = "John", age = 18};
user.Name = "John Ritter"; // no error
user.Age = 20; // error! CS8852.
- Use
readonly
for members which don't modify state, e.g.ToString()
[C#8]
✔️ Pros: specifies the design intent so the compiler can enforce it, and make optimizations based on that intent.
- Use partial class to split the implementation of different interfaces
// 📄 MyClass.cs
partial class MyClass{}
// 📄 MyClassIF1.cs
partial class MyClass : IF1 {}
// 📄 MyClassIF2.cs
partial class MyClass : IF2 {}
- Use tuple in class constructor [C#7]
class User {
public string Name {get; set};
public int Age {get; set};
// 👎 longhand
public User(string name, int age){
_name = name;
_age = age;
}
// 👍 shorthand
public User(string name, int age) => (_name, _age) = (name, age);
}
- Create extension methods for commonly used operations on value types or existing classes
✔️ Pros: reduce one parameter in utility method for value types, add new functions for a class without modifying/inheriting it
namespace ExtensionMethods;
public static class IntUtils
{
public static bool IsGreaterThan(this int i, int value) => i > value;
public static bool IsPrime...
}
public static class StringUtils
{
public static int GetWordCount(this string word) => str.Split(new char[] { ' ', '.', '?' }, StringSplitOptions.RemoveEmptyEntries).Length;
}
Usage:
using ExtensionMethods;
int i = 5;
if(i.IsGreaterThan(3)) System.Console.WriteLine($"{i} is greater than 3"); // output: 5 is greater than 3
string name = "Foo Bar";
System.Console.WriteLine(name.GetWordCount()); // output: 2
- Create extension method taking bool param to write single-statement function w/ single flag in form of expression body
// 👉 given
private AudioSource _bgmSource;
private bool _enableSound;
// 👎 longhand: statement body
public void PlayBgm() {
if(_enableSound) {
_bgmSource.Play();
}
}
// 👍 shorthand: expressionn body
public void PlayBgm() => _bgmSource.Play(@if: _enableSound);
- Implicit method group conversion for callback function
// 👉 given
List<string> users = new {"User 1", "User 2"};
// 👎 longhand
users.ForEach(user => Console.WriteLine(user));
// 👍 shorthand
users.ForEach(Console.WriteLine);
POCO class=> tuple: for returning multiple values from private and internal utility methods [C#7]
- Use
nameof()
to address class/function/param name in string
✔️ Pros: when changing name, the corresspond values in string will update as well
public void PrintUserName(User currentUser)
{
// 👎 the refactoring tool might miss the textual reference to current user below if we're renaming it
currentUser is null && _logger.Error("Argument currentUser is not provided");
// 👍 preference
currentUser is null && _logger.Error($"Argument {nameof(currentUser)} is not provided");
}
-
""=>String.Empty
-
Escape sequence=> verbatim string@
// 👎 non-compliant
string myFileName = "C:\\myfolder\\myfile.txt";
// 👍 preference
string myFileName = @"C:\myfolder\myfile.txt";
- String methods
// whether the specified string is null or an Empty string.
String.IsNullOrEmpty(string value);
// whether a specified string is null, empty, or consists only of white-space characters.
String.IsNullOrWhiteSpace(string value);
List<string> users = new {"User 1", "User 2"};
String.Join(",", users);
- Digit separators: for long numbers [C#7]
// 👎 non-compliant
public const long BillionsAndBillions = 100000000000;
// 👍 preference
public const long BillionsAndBillions = 100_000_000_000;
- Constraint numeral type for generics instead of creating overloading methods as follows: (#generics #numeric)
// 👎 non-compliant
public static int Double(this int number) => number * 2;
public static float Double(this float number) => number * 2;
public static double Double(this double number) => number * 2;
...
// 👍 preference
public static T Double(this T number) where T: unmanaged, IComparable, IComparable<T>, IConvertible, IEquatable<T>, IFormattable
=> (dynamic) number * 2;
Alternative: use code generator or type policy
- Use the as abstract and generic type as possible for creating method
✔️ Pros: cover more types, avoid boxing/unboxing => reduce workload of Garbabe Collection => increase performance
=> generic collection System.Collections.ArrayList
System.Collections.Generic.List<T>
Array, IList => IEnumerable<T> (for method w/o element addition/removal)
List<T> => ICollection<T> (for method w/ element addition/removal)
=>Dictionary.ContainsKey()
Dictionary.TryGetValue()
✔️ Pro: thread-safety, more compact if check & get value
// 👎 non-compliant
if(dictionary.ContainsKey(key))
{
value = dictionary[key];
...
}
// 👍 preference
if(dictionary.TryGetValue(key, out value))
{ ... }
- Use from end operator
^
w/ range operator..
for sequence/collection [C#8]
// 👉 given
string[] words = new string[]
{
// index from start index from end
"The", // 0 ^9
"quick", // 1 ^8
"brown", // 2 ^7
"fox", // 3 ^6
"jumped", // 4 ^5
"over", // 5 ^4
"the", // 6 ^3
"lazy", // 7 ^2
"dog" // 8 ^1
}; // 9 (or words.Length) ^0
To access collection element:
// 👎 longhand
Console.WriteLine($"The last word is {words[words.Length - 1]}");
Console.WriteLine($"The second last word is {words[words.Length - 2]}");
// 👍 shorthand
Console.WriteLine($"The last word is {words[^1]}");
Console.WriteLine($"The second last word is {words[^2]}");
To extract elements:
// 👎 longhand
string[] quickBrownFox = new string[] {words[1], words[2], words[3]};
// 👍 shorthand
string[] quickBrownFox = words[1..4];
// 👍 other examples
string[] allWords = words[..];
string[] lazyDog = words[^2..^0];
string[] theLazyDog = words[6..];
- Make a collection of continuous integers using
Enumerable.Range()
// 👎 longhand
List<int> from2To8 = new {2, 3, 4, 5, 6, 7, 8};
// 👎 longhand
List<int> from2To8 = new();
for(int i = 2; i <= 8; i++) from2To8.Add(i);
// 👍 shorthand
List<int> from2To8 = Enumerable.Range(2, 8).ToList();
- Return empty collection using
Enumerable.Empty<T>()
orArray.Empty<T>()
✔️ Pros: reusable empty instance
// 👎 non-compliant
return null; // 1: force to check null
return new List<User>(); // 2: increase the pressure on the Garbage Collector
// 👍 preference
return Enumerable.Empty<User>();
- Top-level statement: compact Main method [C#9]
// 👎 longhand
using System;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
// 👍 shorthand
using System;
Console.WriteLine("Hello World!");
- File-scoped namespace declaration [C#10]
// 👎 non-compliant
namespace ApplicationA {
class User {
...
}
}
// 👍 preference
namespace ApplicationA;
class User {
...
}
- Namespace alias qualifier
::
✔️ Pros: avoid conflict, abbreviate namespace
// 📄 ApplicationA/User.cs
namespace ApplicationA;
class User {}
// 📄 ApplicationB/User.cs
namespace ApplicationB;
class User {}
// 📄 Driver.cs
using A = ApplicationA;
using B = ApplicationB;
A::User = new();
B::User = new();
- Aliased generics
using
for long & complex types
// 👎 longhand
Dictionary<string, Dictionary<string, List<string>>> userInfo = new();
System.Collections.Generic.LinkedList<User> users = new();
// 👍 shorthand
using UserInfo = Dictionary<string, Dictionary<string, List<string>>>;
using Generic = System.Collection.Generic
UserInfo user = new();
Generic.LinkedList<User> users = new();
-
Global using directive to use namespace in all files of the project [C#10]
-
Region directive=> use partial classes or separate into multiple different classes#region
Reason: Regions are considered anti-patterns since they mostly indicate the class have more then one responsibility.
Base type casting=>System.Convert
Pros: Convert class enables to convert between non-compatible types
// 👉 given
string variable = "5.00";
// 👎 non-compliant
double varDouble = (double)variable; // error: InvalidCastException
// 👍 preference
double varDouble = System.Convert.ToDouble(variable); // no error
Objest casting=>as
✔️ Pros: if non-comatible types, casting throws InvalidCastException while
as
return null
❌ Cons: potential to get NullReferenceException later
// 👎 non-compliant
User instance = (User) mobileUser;
// 👍 preference
User instance = mobileUser as User;