{ claus.conrad }

C# Essential Training 1: Types and Control Flow


Explore the essentials

What you should know

C# compiles to MSIL

  • All .NET languages compile to Microsoft Intermediate Language (MSIL), also known as “managed code”.
  • Therefore the language can evolve independent of the runtime (i.e. developers can benefit from new constructs in [[r.dev.c-sharp|C#]], while the compiled code stays compatible with the already installed runtime).
  • .NET 6 was released in the fall of 2021.

.NET runtime is responsible for running MSIL

  • The runtime can be installed separately from the SDK, similar to how Java can be installed as either the JRE or JDK.
  • More recent versions of the runtime allow it to be deployed together with the application, not requiring a system-wide installation as earlier.
  • Thus applications targeting different runtime versions can co-exist on the same machine.

The base class library

NuGet packages

Compiling C# to a library or executable

  • The command-line compiler is called csc.exe.

Understanding classes, structs, and records

  • Some compiler options (“flags”) can be set in the *.csproj file.
  • C# language versioning
  • Starting in .NET 6, reference types (e.g. objects, including strings) are not nullable by default for new projects.
  • Any type (reference or value) can be allowed to be nullable by declaring it with a question mark, e.g.:
    string? name;
  • All implicitly typed local variables (declared using var) are also nullable.
  • classes are reference types, structs are value types.
  • C# 9 introduces records, which are intended to be immutable, e.g. for data to pass around between services.
  • C# 10 introduces record structs, which have the behaviors of value types as well as records.
    • records can be written as record class, to differentiate them from record structs (even though the latter were introduced later).
  • Value types can implement interfaces, but not inherit from a base class.

Defining constructors

  • In Visual Studio, ctor is a keyboard shortcut to scaffold a constructor (stub).
  • The constructor method has the same name as the class.
  • A constructor can invoke the constructor of its base class like this:
    public ClassA() : base("param1") { }
  • Since C# 6, you can prefix a string with a $ sign to have embedded variables (within {}) interpolated.
  • In a method signature, default values can be provided for arguments, making them optional.

Object initialization

  • Instead of using a constructor to initialize (create) an object, we can use object initialization to set any property we want. Notice the curly braces instead of the parentheses:
    Employee e = new Employee{
      FirstName = "Claus",
      LastName = "Conrad";
  • Object initialization (as shown above) is shorthand for calling the default constructor (without arguments) and then setting the properties.
  • It can also be combined with another constructor, e.g.
    Employee e = new Employee("Claus"){
      LastName = "Conrad";
  • “Roslyn” is the name of a specific C# compiler that is itself implemented in C# (similar to how PyPy is a Python interpreter written in Python).
  • Any constructor for a struct needs to set a value for all properties (since a struct is a value type).

Initialize only properties

  • A property declared with init instead of set can only be set by a constructor or using object initialization:
    public byte CustomerLevel { get; init; }
  • This is often used on record types to make their values immutable.
  • The access of get and set can be controlled, e.g.
    public int YearsOld { get; private set; }

Cloning and copying objects

  • A reference type (such as an instance of a class, a.k.a. an object) can have multiple variables pointing at it.
    • By default, when a (reference type) variable is passed to a method and the method assigns something else to the variable, that change does not persist after the method returns.
    • This behavior can be changed by prefixing the variable with ref - both in the method definition (signature) and when calling it (argument list). In this case, if the method changes what “object” the (reference type) variable points at, the original variable is modified, and so the change persists after the method returns.
  • When a new variable is assigned a struct (i.e. a value type), then the value is copied, and the two variables (while they initially have the same value) can be modified independently from here on.
    • The same applies when a struct is passed to a method - a copy local to the method is created, and changes to it do not persist after the method returns.
    • This behavior can be changed by prefixing the variable with ref - both in the method definition (signature) and when calling it (argument list). In this case, if the method changes the “struct” (the value type), the original value is modified, and so the change persists after the method returns.
  • The with keyword can be used to copy a record with modifications, e.g.
    public record Customer { /* ... */ }
    Customer c = new Customer{ FirstName = "Claus", LastName = "Conrad" };
    Customer c2 = c with { Lastname = "Clausen" };

Equality comparisons

  • A record can be defined using two different syntaxes:
    • Similar to a class:
      public record RClassLike
         public int Id { get; set; }
    • Shorthand syntax:
      public record RShorthand(int Id);
    • In the shorthand example, this defines the record, its constructor and its single property at the same time. The Id can only be set using this constructor (or using this constructor and an object initializer, but that does not make sense here) and is read-only afterwards (i.e. this syntax is similar to using the init keyword).
  • By default, when comparing for equality,
    • for structs, the equality operator (==) is not defined (but the developer can implement it);
    • for class instances, equality means whether they point to the same object;
    • for records, the framework checks whether all properties have the same value.
  • When overriding/implementing the ==/!= operators, one should also override/implement the Equals() method.

Defining abstract classes