C# Notes
Links
Contents
Terms
Variance
See more here from the MS Docs.
Covariance
Allows interface methods to have more derived return types that that defined by the generic type parameters.
Contravariance
Contravariance allows interface methods to have argument types that are less derived than that specified by the generic parameters.
Variant
A generic interface that has covariant or contravariant generic type parameters is called variant.
Types
Value types
Simple:
sbyte
,short
,int
,long
byte
,ushort
,uint
,ulong
char
- UTF-16float
,double
decimal
- high precisionbool
Enum types
User-defined types of the form
enum E {...}
Struct types
User-defined types of the form
struct S {...}
Nullable value types
Extensions of all other value types with a
null
value
Reference types
Class types
Ultimate base class of all other types:
object
Unicode strings:
string
- UTF-16User-defined types of the form
class C {...}
Interface types
User-defined types of the form
interface I {...}
Array types
Single and multi-dimensional, for example,
int[]
andint[,]
Delegate types
User-defined types of the form delegate
int D(...)
Struct
They are value types - pass by value to methods hence field changes don't persist
Can implement interfaces, can be used as a nullable type and assigned
null
Does not support
protected
since it cannot inherit classes etc.Fields can't be initialized on declaration unless
static
orconst
Generally less expensive than classes and used for small objects/entities
public struct CoOrds
{
public int x, y;
// You can't have an empty constructor, all fields must be initialized
public CoOrds(int p1, int p2)
{
x = p1;
y = p2;
}
}
CoOrds point = new CoOrds();
// You can omit the new keyword since structs are value types
// All fields must be initialized before using
CoOrds point = new CoOrds(1, 1);
CoOrds point;
point.x = 1;
point.y = 1;
Remember that you can override the ToString()
method since everything inherits object
.
Enum
They are converted to numeric literals at compile time hence if you refer to enums or constants from external sources, this may cause potential issues. They can be of the following types: byte
, sbyte
, short
, ushort
, int
, uint
, long
, or ulong
.
enum Day : byte {Sat=1, Sun, Mon, Tue, Wed, Thu, Fri};
int y = (int)Day.Fri; // Explicit cast is necessary
enum Range : long { Max = 2147483648L, Min = 255L };
You can do more interesting things to print the string value and combinations of enums with System.FlagsAttribute.
Implicit types
Make the compiler infer the type from the RHS expression using
var
Can only be used when a local variable is declared and initialized in the same statement; the variable cannot be initialized to null, or to a method group or an anonymous function
Cannot be used on fields at class scope
// a is compiled as int[]
var a = new[] { 0, 1, 2 };
// anon is compiled as an anonymous type
var anon = new { Name = "Terry", Age = 34 };
anon.Name;
foreach(var item in list) {...}
// More interesting with LINQ
var words = new[] { "aPPLE", "BlUeBeRrY", "cHeRry" };
var upperLowerWords =
from w in words
select new { Upper = w.ToUpper(), Lower = w.ToLower() };
// Execute the query
foreach (var ul in upperLowerWords)
{
Console.WriteLine("Uppercase: {0}, Lowercase: {1}", ul.Upper, ul.Lower);
}
Boxing
Boxing is the process of converting a value type to the type object
or to any interface type implemented by this value type. When the CLR boxes a value type, it wraps the value inside a System.Object
and stores it on the managed heap. Unboxing extracts the value type from the object. Boxing is implicit; unboxing is explicit. This comes at a large performance cost at runtime.
int i = 123;
// The following line boxes i.
object o = i;
//The object o can then be unboxed and assigned to integer variable i:
o = 123;
i = (int)o; // unboxing
Dynamic
These are reference types that are not checked at compile time so expressions have limited validity checks. Dynamic also behaves like object
in cases like somevar is dynamic
will always return true
unless somevar
is null
.
static void Main(string[] args)
{
ExampleClass ec = new ExampleClass();
// The following call to exampleMethod1 causes a compiler error
// if exampleMethod1 has only one parameter. Uncomment the line
// to see the error.
//ec.exampleMethod1(10, 4);
dynamic dynamic_ec = new ExampleClass();
// The following line is not identified as an error by the
// compiler, but it causes a run-time exception.
dynamic_ec.exampleMethod1(10, 4);
// The following calls also do not cause compiler errors, whether
// appropriate methods exist or not.
dynamic_ec.someMethod("some argument", 7, null);
dynamic_ec.nonexistentMethod();
}
Nested types
This is more seen as nested classes. You can have a nested class which can access all the members of the parent class.
public class Container
{
public class Nested
{
private Container parent; // needs to be initialized
// default constructor
public Nested() {}
public Nested(Container parent)
{
this.parent = parent;
}
}
}
Expressions
Some interesting ones:
new T(...)
: Object and delegate creationnew T(...){...}
: Object creation with initializernew {...}
: Anonymous object initializernew T[...]
: Array creationtypeof(T)
: Obtain Type object for Tchecked(x)
: Evaluate expression in checked contextunchecked(x)
: Evaluate expression in unchecked contextdefault(T)
: Obtain default value of type Tdelegate {...}
: Anonymous function (anonymous method)x is T
: Returntrue
ifx
is aT
,false
otherwiseChecks for inheritance of objects - e.g. downcasting isn't valid unless polymorphism
x as T
: Returnx
typed asT
, ornull
ifx
is not aT
Same as casting but doesn't throw an exception if invalid, returns
null
instead
x ?? y
: Evaluates toy
ifx
isnull
, tox
otherwisex ? y : z
: Evaluatesy
ifx
istrue
,z
ifx
isfalse
(T x) => y
: Anonymous function (lambda expression)
Lambda expressions
A horrible way to do nothing:
delegate void delegateFunc();
private static void Main()
{
delegateFunc f = () =>
{
Console.WriteLine("Delegate & lambda function");
Console.WriteLine("Exiting delegate & lambda function");
}
f();
}
Other maybe more useful e.g. in LINQ:
// Say in a Class
public override string ToString() => $"{fname} {lname}".Trim();
public Point Move(int dx, int dy) => new Point(x + dx, y + dy);
// The Enumerable.Where has two methods overloads
var shortDigits = digits.Where((digit, index) => digit.Length < index);
IEnumerable<string> query = fruits.Where(fruit => fruit.Length < 6);
Operators
Some interesting ones
Null coalescing
// returns the left-hand operand if the operand is not null
// otherwise it returns the right hand operand.
Person p = FooBar ?? DeadBeef;
Null conditional
// used to test for null before performing a member access
int? length = customers?.Length; // null if customers is null
Customer first = customers?[0]; // null if customers is null
int? count = customers?[0]?.Orders?.Count(); // null if customers, the first customer, or Orders is null
Ternary operator
condition ? first_expression : second_expression; // right-associative
int x = (1 == 1) ? 2 : 3;
Bit operations
// Bitwise XOR
int x = 1 ^ 0; // XOR
int x = 1 | 0; // OR
int x = ~1; // NOT
Equality
Make sure you understand that you have value and reference types for equality.
== Operator
Most commonly used for equality but doesn't always behave as expected e.g. for reference types, it will default to reference equality if an overloaded ==
operator method is not found for the type.
// overload operator *
public static Fraction operator *(Fraction a, Fraction b)
{
return new Fraction(a.num * b.num, a.den * b.den);
}
ReferenceEquals
If you only want to compare if the two instances are equal or point to the same instance, use the static
method ReferenceEquals
. Remember, because it is a static method, you cannot override it like virtual methods.
using System;
Test a = new Test() { Num = 1, Str = "Hi" };
Test b = new Test() { Num = 1, Str = "Hi" };
// False
Console.WriteLine("ReferenceEquals(a, b) = {0}",
System.Object.ReferenceEquals(a, b));
b = a;
// True
Console.WriteLine("ReferenceEquals(a, b) = {0}",
System.Object.ReferenceEquals(a, b));
Value types equality
However, sometimes you want to have a custom definition of equality for your value types e.g. struct
Implement the interface
System.IEquatable<T>
which provides type-specific equality!Override the virtual method of
Object.Equals(Object)
- no type safety hereThis usually just calls your type-specific implementation
Implement the
==
and!=
operator methods (call type-specific equality)Implement
GetHashCode()
method
// Generic Object equality
public override bool Equals(object obj)
{
return this.Equals(obj as TwoDPoint);
}
// Type-specific equality
public bool Equals(TwoDPoint p)
{
// If parameter is null, return false.
if (Object.ReferenceEquals(p, null))
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// If run-time types are not exactly the same, return false.
if (this.GetType() != p.GetType())
{
return false;
}
// Return true if the fields match.
// Note that the base class is not invoked because it is
// System.Object, which defines Equals as reference equality.
return (X == p.X) && (Y == p.Y);
}
Reference type equality
You have to care about inheritance when dealing with reference types i.e. in your base class, you perform the standard checks, and then in your derived class, you call the base class check and then call any additional checks as necessary.
Remember that the static object.Equals(object lhs, object rhs)
method for reference types will call any overridden virtual method of object.Equals(object obj)
.
NOTE
// The == operator will never call your overridden virtual method
// of object.Equal() so it will always do a reference check
public CheckIfEqual(object o1, object o2)
{
return o1 == o2;
}
String equality
Use
StringComparison.Ordinal
orStringComparison.OrdinalIgnoreCase
for comparisons as your safe default for culture-agnostic string matching.Use comparisons with
StringComparison.Ordinal
orStringComparison.OrdinalIgnoreCase
for better performance.Use the non-linguistic
StringComparison.Ordinal
orStringComparison.OrdinalIgnoreCase
values instead of string operations based onCultureInfo.InvariantCulture
when the comparison is linguistically irrelevant (symbolic, for example).Do not use string operations based on StringComparison.InvariantCulture in most cases. One of the few exceptions is when you are persisting linguistically meaningful but culturally agnostic data.
Delegates
These are basically function pointer equivalent
// declare this is in some namespace
public delegate int DelegateName(int firstArg, int secondArg);
// now you can instantiate the delegate function in some constructor for example
public class bar
{
DelegateName FunctionPtr;
public bar()
{
FunctionPtr = new DelegateName(foo);
// alternatively you can do this?
// FunctionPtr = foo;
// we can call/(de)attach multiple delegates
// - can no longer reset FunctionPtr
// FunctionPtr += foo2
// FunctionPtr -= foo2
}
// here's a function
public int foo(int x, y)
{
return x + y;
}
// now we can run the delegate in a property for example
private int _sum = 0;
public int PropertyNameSum
{
get
{
return _sum;
}
set
{
_sum = FunctionPtr(0, value);
}
}
}
Func
Another form is the Func<T, TResult> Delegate
, you can have up to 16 input types e.g. Func<T1,T2,T3,T4,T5,T6,T7,T8,T9,T10,T11,T12,T13,T14,T15,T16,TResult>
and an example usage of this with lambda expression:
// Declare a Func variable and assign a lambda expression to the variable.
Func<string, string> selector = str => str.ToUpper();
// Create an array of strings.
string[] words = { "orange", "apple", "Article", "elephant" };
// Query the array and select strings according to the selector method.
IEnumerable<string> aWords = words.Select(selector);
This is useful since you can pass it in as a method parameter without declaring a custom delegate.
// They can also be assigned to an expression/statement lambdas
Func<string, string, int> callback = (s1, s2) => return 1;
int (Func<string, string, int> callBackAsParam)
{
return callback("Foo", "Bar");
}
Expression lambdas
// (input-parameters) => expression
(int x, string s) => s.Length > x;
() => Console.WriteLine("Empty parameter list");
// You can also use method decorators
async innerCancellationToken => await ExecuteAsync(innerCancellationToken);
Statement lambdas
You can have statements in the lambdas but do restrict the number.
delegate bool EqualityComparator(string s1, string s2);
EqualityComparator = (s1, s2) =>
{
if (s1 == null)
throw ArgumentNullException(nameof(s1));
if (s2 == null)
throw ArgumentNullException(nameof(s2));
return string.Equals(s1, s2);
}
Action
If the delegate has no return value i.e. void
, you can use Action<T>
Action<string> print = Console.WriteLine;
Action<string, string> concat = (s1, s2) => Console.WriteLine(${s1}{s2});
Closure
A closure is basically a block of code (lambda/anonymous function) that can be executed at a later time but maintains the environment which it was first created. This allows it to continue reference and hold state of those variables in the containing method.
Basically the C# compiler will recognize the closure being created and will promote the local variables into a compiler generated class which the delegate can be invoked and maintain and use the local variables. This means if you were to create a closure on a disposable or mutatable object, you can easily hit non-deterministic behavior with NullReferenceException
.
static void Main(string[] args)
{
var inc = GetAFunc();
Console.WriteLine(inc(5));
Console.WriteLine(inc(6));
}
public static Func<int,int> GetAFunc()
{
var myVar = 1;
Func<int, int> inc = delegate(int var1)
{
myVar = myVar + 1;
return var1 + myVar;
};
return inc;
}
// 7
// 9
Exceptions
try
{
int x = 10 / 0;
}
catch (DivideByZero ex)
{
Console.WriteLine("Trying to divide by zero");
}
catch (InvalidOperation ex)
{
}
Catching multiple exceptions
try
{
int x = 10 / 0;
}
catch (Exception ex) when (ex is InvalidOperation || ex is NullPointerException)
{
Console.WriteLine($"Hit exception {nameof(ex)}");
}
Creating your own exceptions
It's important to implement all constructors when creating your exception. The MS Docs have a bit more information you can read as well.
[Serializable]
public class ToolNotSupportedException : Exception
{
public ToolNotSupportedException() : base()
{ }
public ToolNotSupportedException(string message) : base(message)
{ }
public ToolNotSupportedException(string message, Exception inner) : base(message, inner)
{ }
protected ToolNotSupportedException(SerializationInfo info, StreamingContext context) : base(info, context)
{ }
}
Misc examples
// goto
static void GoToStatement(string[] args)
{
int i = 0;
goto check;
loop:
Console.WriteLine(args[i++]);
check:
if (i < args.Length)
goto loop;
}
// yield
static IEnumerable<int> Range(int from, int to)
{
for (int i = from; i < to; i++)
{
yield return i;
}
yield break;
}
static void YieldStatement(string[] args)
{
foreach (int i in Range(-10,10))
{
Console.WriteLine(i);
}
}
// checked
static void CheckedUnchecked(string[] args)
{
int x = int.MaxValue;
unchecked
{
Console.WriteLine(x + 1); // Overflow
}
checked
{
Console.WriteLine(x + 1); // Exception
}
}
// lock
class Account
{
decimal balance;
private readonly object sync = new object();
public void Withdraw(decimal amount)
{
lock (sync)
{
if (amount > balance)
{
throw new Exception(
"Insufficient funds");
}
balance -= amount;
}
}
}
// using - auto garbage collection like with in Python
using (TextWriter w = File.CreateText("test.txt"))
{
w.WriteLine("Line one");
w.WriteLine("Line two");
}
Classes
Members of a class can be static
or instance
members. These include:
Constants
Constant values associated with the class
Fields
Variables of the class
Methods
Computations and actions that can be performed by the class
Properties
Actions associated with reading and writing named properties of the class
set {...}
,get {...}
Indexers
Actions associated with indexing instances of the class like an array
Events
Notifications that can be generated by the class
EventHandler
Operators
Conversions and expression operators supported by the class
Constructors
Actions required to initialize instances of the class or the class itself
Finalizers
Actions to perform before instances of the class are permanently discarded
Not recommended since C# has an automatic garbage collection
Types
Nested types declared by the class
Accessibility
public
Access not limited
protected
Access limited to this class or classes derived from this class
internal
Access limited to the current assembly (.exe, .dll, etc.)
protected internal
Access limited to the containing class or classes derived from the containing class
private
Access limited to this class
private protected
Access limited to the containing class or classes derived from the containing type within the same assembly
Defaults
private
class
membersstruct
membersdelegate
within a nested namespace
internal
class
struct
interfaces
delegate
within a namespace
private
interface
members
Sealed
They cannot be used as a base class - prevents derivation and therefore cannot be an
abstract
classYou can use this on a
virtual
method to negate the virtual aspect of a member for any further derived class
public class D : C
{
public sealed override void DoWork() { }
}
Base classes
public class Point
{
public int x, y;
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
}
public class Point3D: Point
{
public int z;
public Point3D(int x, int y, int z) :
base(x, y)
{
this.z = z;
}
}
// up-casting works as intended
Point a = new Point(10, 20);
Point b = new Point3D(10, 20, 30);
Parameters
Like D, it supports both value and reference parameters. Value parameters means that the arguments are initialized as local variables, reference parameters ref
are not!
Reference parameter
using System;
class RefExample
{
static void Swap(ref int x, ref int y)
{
int temp = x;
x = y;
y = temp;
}
public static void SwapExample()
{
int i = 1, j = 2;
Swap(ref i, ref j);
Console.WriteLine($"{i} {j}"); // Outputs "2 1"
}
}
Out parameter
using System;
class OutExample
{
static void Divide(int x, int y, out int result, out int remainder)
{
result = x / y;
remainder = x % y;
}
public static void OutUsage()
{
Divide(10, 3, out int res, out int rem);
Console.WriteLine("{0} {1}", res, rem); // Outputs "3 1"
}
}
}
Parameter array
This is the equivalent of args*
in Python. You can use this to pass an optional number of arguments to a funciton.
public class Console
{
public static void Write(string fmt, params object[] args) { }
public static void WriteLine(string fmt, params object[] args) { }
// ...
}
Console.WriteLine("x={0} y={1} z={2}", x, y, z);
// equivalent
string s = "x={0} y={1} z={2}";
object[] args = new object[3];
args[0] = x;
args[1] = y;
args[2] = z;
Console.WriteLine(s, args);
Methods
Remember that static
methods cannot access instance methods while instance methods can access both since static
methods belong to the class.
Virtual
virtual
modifierrun-time type of instance is determines the actual method implementation
these methods can be modified in a derived class
you cannot use these modifiers with virtual methods:
static
,abstract
,override
Non-virtual
default method types
compile-time type of instance is determines the actual method implementation
Abstract
they are
virtual
methods with no implementation and can only belong in anabstract
classthey must be overridden in a non-abstract derived class
A basic use case:
using System;
using System.Collections.Generic;
public abstract class Expression
{
public abstract double Evaluate(Dictionary<string,object> vars);
}
public class Constant: Expression
{
double value;
public Constant(double value)
{
this.value = value;
}
// overriding abstract methods in a derived class is mandatory
public override double Evaluate(Dictionary<string,object> vars)
{
return value;
}
}
Example of overriding a virtual method:
public class Base
{
public virtual void Foo()
{
Console.WriteLine("base");
}
}
public class Derived : Base
{
public override void Foo()
{
Console.WriteLine("derived");
}
}
Abstract
Can contain partial implementations of a class - thus cannot be instantiated
An
abstract
class must provide implementation for all interface membersOften paired with
override
andvirtual
- actually abstract methods implicitly virtualRemember; an
abstract
method has no implementation but anabstract
class can have virtual or non-virtual methodsabstract
methods can only exist inabstract
classesIf a class derives from an
abstract
class, check permissions e.g.private
->protected
You can only inherit from a single
abstract
class
For abstract
methods, you can couple this with override
in a derived class to override a virtual
method with an abstract
method.
public class D
{
public virtual void DoWork(int i)
{
// Original implementation.
}
}
public abstract class E : D
{
public abstract override void DoWork(int i);
}
public class F : E
{
public override void DoWork(int i)
{
// Force new implementation.
}
}
Partial
You can split class implementations using partial
but they must be in the same namespace
. Good thing namespaces can be split across multiple files as well. It is also applicable to struct
and interface
which is a bit more relevant.
// Foo.cs
namespace ns
{
public partial PartClass
{
public void Foo() {}
}
}
// Bar.cs
namespace ns
{
public partial PartClass
{
public void Bar() {}
}
}
partial struct S1
{
void Struct_Test() { }
}
partial struct S1
{
void Struct_Test2() { }
}
This is also applicable to methods where the method can be implemented in a different file. This is quite confusing and it's better to use interfaces or abstract classes.
Property
These are members that are special methods called accessors. They can be either or both get
and set
accessors
class TimePeriod
{
// We can use a default property
public int Day {get; set; }
public double Hours
{
// We can use lambda expressions
// We are implicitly setting as read-write
get => return seconds / 3600;
set
{
if (value < 0 || value > 24)
throw new ArgumentOutOfRangeException($"Invalid value");
}
}
}
Other misc.
// abstract modifier forces the derived class to implement it
public abstract int MyIntProperty
{
get; // Read-only since isn't declared
}
// Restrict access to the set accessor
// When inheriting or interface implementation, access level
// must be consistent and respected
public override int TestProperty
{
protected set
{
name = value;
}
}
We can also use explicit interface implementation if a class were to implement multiple interfaces with name collisions e.g.
public interface IEmployee
{
string Name {get; set;}
}
// If it were to implement IMedicalRecord as well
// We can also implement IMedicalRecord.Name
public class Employee : IEmployee
{
private string _name;
string IEmployee.Name
{
get => _name;
set => _name = value;
}
}
Inheritance and Polymorphism
You can use the usual virtual
and override
for inheritance and Polymorphism like normal however even if you upcast, the method being called will be from the derived class.
You can avoid using virtual
by using new
instead but it will no longer call the derived method when upcasting.
public class BaseClass
{
public void DoWork()
{
Console.WriteLine("In BaseClass do work");
}
// Can be overridden multiple times with inheritance
public virtual void DoWorkNormal()
{
Console.WriteLine("In Derived do work");
}
}
public class Derived : BaseClass
{
// If you upcast for example:
// D d = D();
// B b = (B)d;
// b.DoWork() // "In BaseClass do work"
public new void DoWork()
{
Console.WriteLine("In Derived do work");
}
// You normal upcast with polymorphism
// b.DoWork() // "In BaseClass do work"
public override void DoWorkNormal()
{
// You can also access base class methods
// base.DoWorkNormal();
Console.WriteLine("In Derived do work");
}
}
Using the new
keyword tells the compiler that your definition hides the definition that is contained in the base class. This is the default behavior.
Method override and selection can get a little more confusing when you have method overloading. For example:
public class Derived : Base
{
public override void DoWork(int param) { }
public void DoWork(double param) { }
}
Derived d = new Derived();
d.DoWork(val); // Calls DoWork(double)
((Base)d).DoWork(val); // Calls DoWork(int) on Derived.
Reasoning:
Because the variable
val
can be converted to adouble
implicitly, the C# compiler callsDoWork(double)
instead ofDoWork(int)
.Also because the
DoWork(int)
is an overridden method, it isn't considered as declared on a class - they are new implementations of a method declared on a base class. Thus the compiler will first try to make the call on methods compatible with the versions ofDoWork
originally declared inDerived
.There are two ways to avoid this.
First, avoid declaring new methods with the same name as virtual methods.
Second, you can instruct the C# compiler to call the virtual method by making it search the base class method list by casting the instance of
Derived
toBase
. Because the method is virtual, the implementation ofDoWork(int)
onDerived
will be called.
This behavior for the
new
modifier is applicable to properties as well
Constructors
You can control the base class constructor when the derived class constructor is called. This can be useful for constructor reuse. You can also call multiple constructors of this
and avoid duplication.
public class BaseClass
{
// BaseClass requires an integer argument
public BaseClass(int i) {}
}
public class Derived : BaseClass
{
// Here you need to pass an integer to the BaseClass constructor
// otherwise you will have a compile error
// public Derived() : base(10)
// { }
// Instead of the above, you can do constructor reuse
// And have more flexibility
public Derived() : this(10)
{ }
public Derived(int i) : base(i)
{ }
}
You can have private
constructors and this is common to restrict default constructors. This doesn't prevent you from declaring additional public constructors. It can be used to enforce a particular pattern, singleton etc.
We can also have something called "copy" constructors where you copy all the state of an instance to generate a new instance based off it. This simply saves writing a separate function for getting and setting state.
class Person
{
// Copy constructor
public Person(Person previousPerson)
{
Name = previousPerson.Name;
Age = previousPerson.Age;
}
...
}
// Trivial example but the state could be must more complicated
// and tedious, hence is applicability
Person person1 = new Person("George", 40);
Person person2 = new Person(person1);
person2.Name = "New Person's name";
Finalizes
You can cleanup objects or resources by using:
Dispose
finally
using
// They are not inherited and are called automatically
// They are put in the Finalize queue for the garbage collector
// And derived classes they are recursively cleaned up
public class Car
{
~Car()
{
Console.WriteLine("Destructor, can only have one per class");
}
}
You don't want to use the Dispose
method unless you know what you're doing. Read more about it:
Interfaces
Can only contain
abstract
methods and are all not implementedCan contain: methods, properties, events, indexers
Methods are default
public
but can beabstract
Cannot contain: constants, fields, operators, instance constructors, finalizers, or types
If you are inheriting a base class and also implementing an interface, inheritance comes first
Interface members are automatically
public
, and they can't include any access modifiers. Members also can't bestatic
Explicit interface
You can have an explicit method in an interface where the method is only called if we are programming against the interface. For example:
interface FooInterface
{
string FooInterface.save()
{
Console.WriteLine("FooInterface save()");
}
}
public class BarClass : FooInterface
{
}
// Somewhere else
BarClass bar = new BarClass();
bar.save() // COMPILE ERROR
FooInterface bar = new BarClass();
bar.save() // "FooInterface save()"
Static
We can use static
constructors to initialize static objects e.g. readonly
members at compile time and is only run at most once. They also run before any instance constructors and the user cannot access it directly nor control when they are run.
public class Adult : Person
{
private static int minimumAge;
public Adult(string lastName, string firstName) : base(lastName, firstName)
{ }
// You can use a lambda expression here
static Adult()
{
minimumAge = 18;
}
// Remaining implementation of Adult class.
}
You can have static classes which can be useful to contain just static
or const
members like the Math
class.
public static class TemperatureConverter
{
// Have some useful methods that doesn't need to belong to an instance
public static double CelsiusToFahrenheit(string temperatureCelsius) {}
public static double FahrenheitToCelsius(string temperatureFahrenheit) {}
}
Enumerating
If you want an object e.g. your class to be enumerable, you can derive a class that implements the IEnumerable
or implemented the following methods:
public IEnumerator GetEnumerator()
public bool MoveNext()
public void Reset()
public object Current
- if you don't implement the interface, you can return a more specific objectYou need to at least implement the
GetEnumerator
method, the others will be automatically generated
You can also define a method that returns an IEnumerable
and contains the yield return
keywords.
Indexers
This allows you to index an object without creating an array of objects. They can be overloaded and have more than one parameter e.g. multi-dimensional arrays. The indexer doesn't even need to contain an internal array, you could use it to compute some value based on the index. There are some differences with properties that the MS docs describe.
// We can optionally declare an indexer in an interface
// much like properties
interface IIndexer<TType>
{
TType this[int i] { get; set; }
}
class IndexedClass<TType> : IIndexer
{
private TType[] InternalArray;
public IndexedClass(int arraySize = 10)
{
InternalArray = new TType[arraySize];
}
// Indexes are similar to properties
public TType this[int i]
{
get { return this.InternalArray[i]; }
// You can have a read-only indexer as well and control the conditions
// in the new method
set => this.InternalArray[i] = value;
}
}
Events
They are a way of support subscribers and publishers model.
Generics
Too much to cover about generics, see the MS docs for information
Generics at run time are actually optimized so there is instance reuse for reference classes (references have the same value - pointers). This is slightly less optimized for value types as a specialized version of the class is initialized per value type and then reused.
LINQ
It's important to know that you can use either query expression and method-based query for the same result. However, some queries must be method-based so it is important that you are familiar with both options. You can also use var
to let the compiler infer types however it is generally better to explicitly define types for clarity.
Queries can also return anonymous types e.g. ... select new { name = cust.Name, phone = cut.Phone};
, here the use of var
is imperative.
int[] numbers = { 5, 10, 8, 3, 6, 12};
// Query syntax:
IEnumerable<int> numQuery1 =
from num in numbers
where num % 2 == 0
orderby num
select num;
// Method syntax:
IEnumerable<int> numQuery2 = numbers
.Where(num => num % 2 == 0)
.OrderBy(n => n);
foreach (int i in numQuery1)
{
Console.Write(i + " ");
}
Console.WriteLine(System.Environment.NewLine);
foreach (int i in numQuery2)
{
Console.Write(i + " ");
}
// Something more interesting
var studentQuery8 =
from student in students
let x = student.Scores[0] + student.Scores[1] +
student.Scores[2] + student.Scores[3]
where x > averageScore
select new { id = student.ID, score = x };
The List<int> numbers
now has the Where
method due to method extensions from LINQ.
Any vs Count
For IEnumerable<>
, use Any()
.
For collections like IList<>
etc. use the Count
property.
See also Stack Overflow - Any vs Count.
Namespaces
They organize large code projects.
They are delimited by using the
.
operator.The
using directive
obviates the requirement to specify the name of the namespace for every class.The
global
namespace is the "root" namespace:global::System
will always refer to the .NET Framework namespaceSystem
.If there is any ambiguity if you import two namespaces with name collisions, you will get a compile error.
namespace N1 // N1
{
class C1 // N1.C1
{
class C2 // N1.C1.C2
{}
}
namespace N2 // N1.N2
{
class C2 // N1.N2.C2
{}
}
}
// Can also use aliases
using colAlias = System.Collections;
namespace System
{
colAlias::Hashtable test = new colAlias::Hashtable();
global::System.Console.WriteLine(name + " " + test[name]);
}
Null
There's a whole bunch of stuff dealing with null
- basically C# attempts to provide helper functions and features to avoid magic numbers and redundant code.
C# operators:
??
- null coalescing operator?.
- null conditional operator?:
- normal ternary operator
C# methods:
HasValue
: same as!= null
but use either consistently - use this with casting nullable types to non-nullable
// Using t
Person p = null;
// Normal use of ternary operator although it is limiting
// and can be quite cluttered
int nonNullableInt = (p != null) ? 0
: -1;
// Null conditional operator which only evaluates further if
// the right hand side is not null else returns null
int? nullableInt = p?.Age;
// Null coalescing operator which evaluates to the left hand
// expression if not null else right hand expression
p = new Person();
int? nullableInt = p.Age ?? -1;
// Combining null conditional and null coalescing is
// a common practice since they complement each other
p = null;
int? nullableInt = p.?Age ?? -1;
// Arrays with null conditional
Person[] people = new Person[3]
{
new Person(1), // Age = 1
new Person(), // Age = null
null // Person is null
}
int p1 = people?[0].Age; // 1
int? p2 = people?[1].Age; // null
int? p3 = people?[2].Age; // null
// A horrible way of nullable types
Nullable<int> nullableInt;
Helpers for strings
C# methods:
isNullOrEmpty
isNullOrWhiteSpace
: this will also check if the string is empty
Async
C# has asynchronous support built-in to its language and Microsoft encourages the use of it to alleviate the complexity of concurrency, multi-threading, locking etc.
With async programming, firstly, you must identify the following situations:
Is my work IO bound?
Is my work CPU bound?
Is my work worth having async?
Think of await
as an "asynchronous wait" i.e. it blocks the method (if necessary) but not the thread.
Making async synchronous
These articles summarizes it far better than I could have:
Understanding what synchronization context is captured upon awaiting is important as it can cause deadlocks!
.Result
will wait and wrap any exceptions into an AggregateException.Wait()
may do the same as above or swallow exceptions
Eliding == omitting
Must reads:
ConfigureAwait in-dept - MS Blog ConfigureAwait FAQ
Running an async task synchronously - Stack Overflow - How would I run an async task method synchronously
CPU bound
Because it's CPU bound, you ideally want to move this work onto a new thread without making the main thread wait. This work is usually delegated to the thread pool.
You want to use:
await Task.Run(() => Func());
Make sure to yield control back to the main thread!
IO Bound
You don't want a new thread or queue an IO bound task to the Threadpool
because a new thread dedicated to just wait for return request is costly when the thread could be doing more meaningful work!
What you want is to delegate the work to the OS which in turn delegates the work asynchronously - this is important so that when your HTTP request finally responds - it is bubbled back to the caller via interrupts and in a non-blocking fashion.
public async Task DoMixedWork()
{
// Queues the specified work to run on the thread pool
// and returns a Task object that represents that work.
// Don't block the main/current thread
Task cpuTask Task.Run(() => Prime95());
// IO bound work which does not require a new thread
// The task is immediately fired
Task ioTask = SomeIoBoundAsync();
// Do synchronous code that doesn't rely on the above
// return values/work
DoSomeUnRelatedCode();
// Now we need the results
int age = await cpuTask;
int name = await ioTask;
}
public async Task<string> SomeIoBoundAsync()
{
// Fetch some database or HTTP requests
}
public async Task<int> Prime95()
{
// Do lots of very long calculations
return 1*1;
}
Floating point
Common confusions between:
float
/single
: 32 bit - float is an alias to singleApproximately ±1.5e-45 to ±3.4e38 with ~6-9 significant figures
double
: 64 bitFor very large numbers where losing precision is acceptable e.g. very large scientific values
Approximately ±5.0e-324 to ±1.7e308 with ~15-17 significant figures
decimal
: 128 bitUse for money
Smaller range of values, performance cost, stored as base 10
Approximately ±1.0e-28 to ±7.9e28 with 28-29 significant figures
All values except decimal
are stored as base 2 which can leverage hardware acceleration.
Last updated