Code exists on many levels. Abstraction is a ladder. At the highest levels of abstraction, we have C# code. At a lower level, we have an intermediate language.
C# is compiled into that intermediate language.
Notes from Brandon Minnick’s Correcting Common Async/Await mistakes in .NET
By default, all statements marked as await
have the ConfigureAwait(true)
flag. By setting the value to false
, we are explicitly saying that we do not wish to return back to the thread that called us.
Instead, we find any available thread and assign the following tasks to it. This is what the Context Switch does.
This is essential since we don’t want to lock the main thread – which is responsible for dealing with user interactions and the UI.
async Task GetAllPosts()
{
List<Posts> posts = await GetPosts().ConfigureAwait(false);
foreach (var post in posts)
Console.WriteLine(post.Name);
}
// thread 1: initializes the post variable
// thread 1: encounters await, spawns new thread
// thread 2: assigns a new thread to be executed after the current one finishes
// thread 2: finishes to GetPosts(), calls thread 3
// thread 3: continues with the instructions
if we don’t need to return back to the calling thread, use it.
We should use it almost always. Unless we’re dealing directly with the interface, we use it.
Never, ever use it.
It locks the calling thread until the current thread is done, which is not ideal.
async Task<List<Posts>> GetAllPosts()
{
List<Posts> posts = await GetAllPosts().Wait();
if (posts == null || posts.Count() == 0)
return new List<Posts>();
return posts;
}
// thread 1: initializes the posts variables
// thread 1: encounters the await keyword
// thread 1: spawns another thread: thread 2
// thread 2: locks the calling thread (thread 1) until GetAllPosts() is done
// thread 2: finishes the GetAllPosts() operation, free thread 1
// thread 1: continues with the instructions
- never use
async void
. When inside atry/catch
block, the main thread just continues to execute. - never use
.Wait()
or.Result()
- if synchronous, use
.GetAwaiter().GetResult()
- always use
async
Using the Generics lib:
using System.Collections.Generic;
We can define a navigation property to store multiple entities (list):
public virtual ICollection<Enrollment> Enrollments { get; set; }
It’s defined as virtual
so they can use the EntityFramework functionality, like slow loading.
Finalizers (destructors in general) are used to perform necessary clean-ups when a class instance is being collected by the garbage collector.
class Car {
~Car { // finalizer
// cleanup statements
}
}
A finalizer can also be defined as an expression body definition:
public class Destroyer {
~Destroyer() => Console.WriteLine($"The {GetType().Name} destructor is executing.");
}
Dynamic binding defers /binding/ - the process of resolving types, members, and operations from compile time to runtime.
Dynamic binding is useful when at compile time you know that a certain function, member, or operation exists, but the compiler does not.
dynamic d = GetsomeObject();
d.Quack();
We expect the runtime type of d to have a Quack method. We just can’t prove it statically. Since d is dynamic, the compiler deferes binding Quack to d until runtime.
Extension methods allows for an existing type to be extended with new methods without altering the definition of the original type.
public static class StringHelper
{
public static void IsCapitalized(this string s)
{
if (string.IsNullOrEmpty(s)) return false;
return char.IsUpper(s[0]);
}
}
Console.WriteLine("Perth".IsCapitalized());
global
applies to the entire machine
assembly
what .NET calls its code-libraries (DLLs)
cache
a place to store things for common/faster access
Basically, it’s a way to keep DLLs globally accessible without worrying about conflicts.
Everything is located at C:\\Windows\assembly
So the GAC must be a place to store code libraries so they’re accessible to all applications running on the machine.
Interfaces are “signatures” of an object, they include the behavior from multiple sources in a class.
interface IEquatable<T>
{
bool Equals(T obj);
}
public class Car : IEquatable<Car>
{
// implementation of IEquatable<T> interface
public bool Equals(Car car)
{
return true;
}
}
- Follows the same syntax as classes
- Within a
struct
declaration, fields cannot be initialized unless they are declared asconst
orstatic
- A
struct
cannot declare a parameterless constructor - Unlike classes,
structs
can be initialized without using anew
operator. - A
struct
cannot inherit from anotherstruct
orclass
. - A
struct
can implement interfaces - A
struct
cannot be null.
public struct TestStruct {
// Fields, methods, properties and events ...
}
public class Keywords
{
public static void Main()
{
int val1 = 0; // must be assigned a value;
int val2; // optional
Keywords1(ref val1);
Console.WriteLine(val1);
Keywords2(out val2);
Console.WriteLine(val2);
}
static void Keywords1(ref int value) => value = 7;
static void Keywords2(out int value) => value = 9; // must be defined
}
/* Output
7
9
*/
Ref
keywords are used to pass an argument as a reference, meaning that when the value of that parameter
changes after being passed through the method, the new value is reflected in the new calling method.
Out
keywords is similar to ref
in that, they are used to pass an argument, but they differ in that
arguments passed using out
keyword can be passed without any value to be assigned to it.
// optional arguments
public void ExampleMethod(int required, int optionalStr = "default string") { }
static void Main()
{
// named arguments
PrintOrderDetails(sellerName: "Gift Shop", 31, productName: "Red Mug");
}
static void PrintOrderDetails(string sellerName, int orderNum, string productName) {}
Reflection is a C# language mechanism for accessing dynamic object properties in runtime. Typically, reflection is used to fetch the information about dynamic object type and object attribute values.
using System;
using System.Reflection;
using System.Reflection.Linq;
public class Program {
public static void Main()
{
var members = typeof(object)
.GetMembers(Bindingflags.Public |
BindingFlags.Static |
BindingFlags.Instance);
foreach (var member in members)
{
bool inherited = member.DeclaringType.Equals(typeof(object).Name);
Console.WriteLine($"{member.Name} is a {member.MemberType}," +
$"it has {(inherited ? "" : "not")} been inherited.");
}
}
}
using System;
public class Program {
public static void Main()
{
var theString = "hello";
var method = theString
.GetType()
.GetMethod("Substring",
new [] { typeof(int), typeof(int) }); // the types of the method
var result = method.Invoke(theString, new object[] { 0, 4 });
Console.WriteLine(result);
}
}
var method = typeof(Math).getMethod("Exp");
var result = method.Invoke(null, new object[] { 2 }); // pass null as the first argument (no need for an instance)
Console.WriteLine(result); // e^2
The stack
is responsible for keeping track on what’s executing in our code.
The heap
is more or less responsible for keeping track of our objects (data).
Essentially, we have four main things we’ll be putting in the stack and heap as our code is executing:
- Value types (
bool, byte, char, float, int
etc) - Reference Types (
class, interface, [[*Delegates][delegate]], object, string
) -> HEAP - Pointers (reference to a type)
- Instructions
Considering the following code:
public int AddFive(int pValue)
{
int result;
result = pValue + 5;
return result;
}
AddFive()
andpValue | int
goes into the stack.- JIT compiles and executes the first instruction (
AddFive
). - As the method executes, we need some memory for the
result
variable and it is allocated in the stack. - The method finished the execution and the result is returned.
- All memory allocated on the stack is cleaned up by moving a pointer to the available memory address.
Value Types always go where they are declared, if a value type is declared outside a method, but inside a reference type it will be placed within the Reference Type of the heap.
public class MyInt
{
public int MyValue;
}
public MyInt AddFive(int pValue)
{
MyInt result new MyInt();
result.Myvalue = pValue + 5;
return result;
}
- Thread starts executing the method and its parameters are placed on the thread’s stack.
- Because
MyInt
is a Reference Type, it is placed on the Heap and referenced by a Pointer on the Stack. - After
AddFive()
is finished executing, we clean it up. - We’ve left with an orphaned
MyInt
in the heap (there is no longer anyone in the stack pointing toMyInt
)
Delegates are types that represent a reference to a method. They are used for passing arguments as reference to other methods.
class DelegateExample {
public void Run()
{
// using class method
InvokeDelegate(WriteToConsole);
// using anonymous method
DelegateInvoker di = delegate (string input)
{
Console.WriteLine(string.Format("di: {0}", input));
return true;
}
InvokeDelegate(di);
// using lambda expression;
InvokeDelegate(input => true);
}
public delegate bool DelegateInvoker(string input);
public void InvokeDelegate(DelegateInvoker func)
{
var ret = func("Hello world");
Console.WriteLine("> Delegate returned {ret}");
}
public bool WriteToConsole(string input)
{
Console.WriteLine($"WriteToConsole: {input}");
return true;
}
}
Creating and starting a second thread:
using System;
using System.Threading;
class Program {
static void Main()
{
var thread = new Thread(Secondary);
thread.Start();
}
static void Secondary() => Console.WriteLine("Hello world");
}
When the order of the output is not important, we can apply multiple threads to a foreach loop:
using System;
using System.Threading;
using System.Threading.Tasks;
public class Program {
public static void Main() {
int[] Numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// single-threaded:
Console.WriteLine("Normal foreach loop: ");
foreach (var number in Numbers)
Console.WriteLine(longCalculation(number));
// This is the Parallel (Multi-threaded solution):
Console.WriteLine("Multiple threads: ");
Parallel.forEach(Numbers, number =>
Console.WriteLine(longCalculation(number));
);
}
private static int longCalculation(int number)
{
Thread.Sleep(1000); // sleep to simulate a long calculation
return number * number;
}
}
A String is immutable, meaning String cannot be changed once created.
StringBuilder sb = new StringBuilder();
// or
StringBuilder sb = new StringBuilder("Hello world!!");
// allocate 50 bytes sequentially onto the memory heap.
StringBuilder sb = new StringBuilder("Hello world", 50);
Method Name | Description |
---|---|
StringBuilder.Append(valueToAppend) | Appends the passed values to the end of the current String |
StringBuilder.AppendFormat() | Replaces a format specifier passed in a string with formatted text |
StringBuilder.Insert(index, value) | Inserts a string at the specified index of the current object |
StringBuilder.Remove(int start, int length) | Removes the specified number of characters from the given position |
StringBuilder.Replace(old, new) | Replaces characters with new characters |
StringBuilder sb = new StringBuilder("hello", 50);
sb.Append("World!");
sb.AppendLine("Hello C#");
sb.AppendLine("This is a new line.");
Console.WriteLine(sb);
StringBuilder sb = new StringBuilder("Hello world", 50);
sb.Insert(5, " C#");
Console.WriteLine(sb);
StringBuilder sb = new StringBuilder("Hello world", 50);
sb.Remove(6, 7);
Console.WriteLine(sb);
StringBuilder sb = new StringBuilder("Hello world", 50);
sb.Replace("World", "C#");
Console.WriteLine(sb);
The null-coalescing operator is used to set a default value for nullable types or reference types.
// to convert a non-nullable type to a nullable type:
int? x = null;
string name = null;
string myname = name ?? "Bruno";
Executes the definition on the right-hand side if the left-hand side operation is not null.
Returns 0
if people is null.
int length = people?.Length ?? 0;