C# Crash Course

An overview of the C# language with some code examples. Intended for those new to C# but with some previous exposure to programming.

Variables

A variable is a container for information. Variables in C# have a type like int or float a name written in camelCase and optionally an initialized value such as = 10.

bool itWorked = true;

byte myByte = 255;

short smallNum = 32767;

int score;
int score = 10;

long usaGDP = 16770000000000;

double anotherPi = 3.141592653589793;

// The trailing f distinguishes it from other numeric
// values containing decimals.
float pi = 3.1415926f; 

decimal length = 1.459m;

// Single quotes for single characters.
char favoriteLetter = 'c'; 

// Double quotes for strings.
string userName = "Eric"; 

Arrays

An array is a collection of data where every member is the same type. We pair a variable type with square brackets [] to define an array. So an array of integers is defined with int[] and an array of strings is string[].

C# arrays should typically be used when you need to store a fixed number of items, as resizing an array can be an expensive operation. When possible you should try to use a List instead of an array!

int[] scores;

// You must define how many items will be in the
// array ahead of time.
int[] scores = new int[10]; 
int[] scores = new int[5] { 100, 95, 85, 15, 27 };

// You don't have to specify the number of items if
// you explicitly set them at array definition.
int[] scores = new int[] { 100, 95, 85, 15, 27 }; 

string[] names = new string[10];

// Set the score at index 0 to 99
scores[0] = 99; 

numberOfScores = scores.Length;

maxScore = scores.MaxValue;

// Multidimensional array
int[,] array2D = new int[,] { {1, 2}, {3, 4}}

// Jagged array (an array of arrays)
int[][] matrix = new int[4][4]; 
matrix[0, 0] = 1;
matrix[0, 1] = 0;
matrix[3, 3] = 1;

Generics

The most common generics in C# are the List and the Dictionary collections. There are also others like Queue, HashSet, or LinkedList which have more narrow functionality. See: https://docs.microsoft.com/en-us/dotnet/standard/collections/

Generics are cool because they help you to write and maintain fewer lines of code. Every List or Dictionary, regardless of what data type it contains, has the same underlying behavior. They contain only the types you define – no unhappy surprises. Generics also have good performance because they avoid the hit that comes with boxing and unboxing. See: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/types/boxing-and-unboxing

Lists

Let’s say you need a list of enemies in a scene. You could define that as List<Enemy> – so a list of objects of type Enemy.

Lists are similar to arrays, however you typically should opt for a List whenever possible. Lists are easier to maintain, have more functionality than arrays, and are less expensive to resize. The angle brackets <T> indicate the type of the data contained in the List.

// Create a new list.
List<string> listOfStrings = new List<string>();

listOfStrings.Add("Hello World!");

// Insert into the list at index #1 
listOfStrings.Insert(1, "Hello Eric!"); 

listOfStrings[0] = "Goodbye cruel world...";

// Remove the string at index #1
listOfStrings.RemoveAt(1); 

// Delete all values
listOfStrings.Clear(); 

int itemsInList = listOfStrings.Count; // 0

Dictonaries

Your basic key-value pair. The dictionary is specified with angle brackets <T,T> where the first item is the type of the key, and the second is the type of the value.

var phoneBook = new Dictionary<string, int>();

phoneBook["Gates, Bill"] = 5550100;
phoneBook["Bezos, Jeff"] = 5551234;

int billsNumber = phoneBook["Gates, Bill"];

// Same dictionary but with values initialized
// instead of set later.
var phoneBook = new Dictionary<string, int>
{ 
    {"BillyG", 5550100},
    {"Bezo$", 5551234}
};

// A dictionary within a dictionary
public Dictionary<string, Dictionary<int, string>> sftp =
new Dictionary<string, Dictionary<int, string>>
{
    {
        "sftp.example.com",
        new Dictionary<int, string>
        {
            { 22, "SFTP/SSH" },
        }
    }
};

Enumerations

Enums are sets of named constants which map to a set of (typically) integers. Enums promote type safety, reduce potential errors that could arise by using strings or integers, and improve code readability. Enums often pair well with switch statements.

The first value of an enum defaults to 0, similar to an array’s index.

enum PlayerClass { Warrior, Thief, Mage, Cleric };

enum Days { Sun, Mon, Tue, Wed, Thu, Fri, Sat };

enum Temperature { Low, Medium, High };
Temperature temp = Temperature.Medium;
int tempValue = (int)temp;
Console.WriteLine("Temperature value is " + tempValue); 
// result is 1

enum Temp { Low = 2, Medium, High };
Temp t = Temp.Medium;
int tValue = (int)t;
Console.WriteLine("Temperature value is " + tValue);
 // result is 3

Control Flow

Loops

For

for (int i = 0; i < 10; i++)
{
    if (x == 3)
      // Skips to the next iteration of the loop
      continue; 

    Console.WriteLine(i);
}
  • Nested For Loops

    for (int row = 0; row < 5; row++)
    {
        for (int col = 0; col < 10; col++)
        {
            Console.Write("*");
        }
    
        Console.WriteLine(); // Wrap to the beginning.
    }
    
    // Produces:
    // **********
    // **********
    // **********
    // **********
    // **********
    
    for (int row = 0; row < 10; row++)
    {
        // Curly braces are optional here.
        for (int col = 0; col < row + 1; col+)
            Console.Write("*");
    
        Console.WriteLine(); // Wrap to the beginning.
    }
    
    // Produces:
    // *
    // **
    // ***
    // ****
    // *****
    // ******
    // etc...
    

Foreach

int[] scores = new int[10];

foreach (int score in scores)
{
    Console.WriteLine("Scores were: " + score);
}

While

int x = 0;

while (x < 10)
{
    Console.WriteLine(x);
    x++;

Do-While

The key point to remember about do-while is that it will always execute the statement body at least once. The do gets done then it checks for a loop condition with while.

int playerNumber;

do
{
    Console.WriteLine("Enter a number between 0 and 10: ");
    string playerResponse = Console.ReadLine();
    playerNumber = Convert.ToInt32(playerResponse);
}
while (playerNumber < 0 || playerNumber > 10);

Switch

int menuChoice = 3;

switch (menuChoice)
{
  case 1:
    Console.WriteLine("Option one");
    break;
  case 2:
    Console.WriteLine("Option two");
    break;
  case 3:
    Console.WriteLine("Option three");
    break;
  default:
    Console.WriteLine("That's not an option!");
    break;
}

If-Else

Curly braces are optional for if statements when the body of the statement is a single line.

if (score >= 100)
    Console.WriteLine("Perfect! You won!");
else if (score < 100 && score >= 65)
{
    // Multiple line statement bodies require curly braces
    Console.WriteLine("Not bad.");
    Console.WriteLine("But you can do better. Try again.");
}
else
    Console.WriteLine("Sorry, you lose.");

if (levelComplete)
    Console.WriteLine("You've beaten the level!");

// Ternary operator
Console.WriteLine((score > 70) ? "You passed!" : "Fail"); 

Public / Private / Protected

public: the member (variable / method / property) can be accessed from anywhere, even outside the class.

private: the member can only be accessed from inside the class.

protected: the member can only be accessed within the class, as well as accessed from any derived classes (that is, child classes which inherit from a parent class).

Methods

In most cases methods are contained (defined) inside a class or struct.

Defining Methods

Methods can be public, private or protected.

Methods either return some type like an int or an Enemy or nothing at all, which is called void. They may (infrequently) be static. The static keyword indicates the method can be called without first instantiating the class it’s in.

If a method takes parameters the type of each parameter must be specified.

// Returns nothing.
public static void CountToTen()
{
    for (int i = 0; i < 10; i++)
        Console.WriteLine(i);
}

// The returns a float.
public float Average()
{
    // Need a float or decimal type for non-integer values
    float total; 

    for (int i = 0; i < 10; i++)
        // Equivalent to total = total + i;
        total += i;

    return total / 10;
}


// Method takes two int parameters and returns nothing.
static void Count(int startingNumber, int numberToCountTo)
{
    for (int i = startingNumber; i <= numberToCountTo; i++)
        Console.WriteLine(i);
}

Calling Methods

To call a method, first instance the class – unless, as noted above, the method is defined as static.

// Calling a standard (instance) method
SomeClass instance = new SomeClass();
instance.InstanceMethod(); // Compiles
instance.StaticMethod(); // Won't compile

instance.InstanceMethodWithArgument(25);

// Calling a static method
SomeClass.StaticMethod(); // Compiles
SomeClass.InstanceMethod(); // Won't compile

SomeClass.StaticMethodWithArgument(25);

Console.WriteLine is an example of a static method in action. WriteLine is a static method of the Console class. Console itself is a static class, which means we can access Console from anywhere in our project.

Classes

A class is a container for methods. It enables object-oriented programming. Consider a Player class. The Player has various things it can do (e.g. methods), like move, shoot, grab game world objects, and so on.

Classes may contain instance variables of any valid type (as in, they exist when the class is instanced). Instance variables may be private (often written with a leading underscore), private or protected.

All classes have a constructor. The constructor initializes the instance variables defined in the class. You may either explicitly define a constructor, or a default will be created in the background on compilation. Custom constructors allow you to set instance variables with custom values. Otherwise all instance variables are initialized with their default value.

Defining Classes

// Empty class. Valid but kinda boring.
class Player
{
}

// A class with a few instance variables.
class Player
{
    private string _name;
    private int _score;
    private int _livesLeft;
}

// A class with instance variables and a constructor.
class Player
{
    private string _name;
    private int _score;
    private int _livesLeft;

    public Player(string name)
    {
      this._name = name;
    }
}

// Methods in a class
class Player
{
    private string _name;
    private int _score;
    private int _livesLeft;

    public Player(string name)
    {
        this._name = name;
    }

    public void AddPoints(int points)
    {
        _score += points;
    }
}

Properties

A class may also have properties. They provide a get and set accessor which either returns a value or sets a value of a private variable. A property is a way to safely expose a private variable to the public space.

// Define a class property called Score
class Player
{
    // Note the visibility is public.
    public int Score { get; set; }
}

// Instance a new Player object with a default score
Player player = new Player("Eric") { score = 100 };

// Change the player score property directly
// without defining a SetScore or GetScore method
player.Score = 95;

// Explicitly defined property exposing a private variable.
class PlayerAlt1
{
    private int _score;

    public int Score
    {
        get => _score;  // public by default
        set => _score = value; // value is just a C# keyword
    }
}

Player playerAlt1 = new PlayerAlt1("Eric") { score = 100 };

// Basically the same behavior. Just more code.
playerAlt1.Score = 95;


// Properties can also refer to methods that modify how the
// the class can get or set the value.
class PlayerAlt2
{
    private int _score;

    public int Score
    {
        get => _score;
        private set => SetScore(value)
    }

    private void SetScore(int value)
    {
        _score = value;

        if (_score >= 100)
            Console.Writeline("Good job!");
    }
}

Instantiating & Using Classes

// The player is a Player type
Player player = new Player("Eric");

// Accessing methods in a class
player.AddPoints(10);
int currentScore = player.GetScore();
Console.WriteLine(currentScore); // Returns 10
Console.WriteLine(player.GetName()); // Returns "Eric"

// See "properties" for a better way to get and set values

Structs

A struct is similar in appearance to a class. They are defined in mostly the same way. However a struct is a value type, while classes are reference types. In practice this means the entire struct is copied when it is assigned from one variable to another. See: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/choosing-between-class-and-struct

struct HealthStruct
{

    private int _health;

    public int Health
    {
        get => _health;
        set => _health = value;
    }

    // Methods can be defined in a struct in the usual way.
    public int CalculateMaxHealth()
    {
        return _health * 100;
    }
}

// Elsewhere...
public static void Main(string[] args)
{
    HealthStruct healthStruct = new HealthStruct();
    healthStruct.Health = 10;

    // This won't change healthStruct's value!
    RegenerateHealth(healthStruct);
}

/* 
 * Here we are receiving a *copy* of the HealthStruct. We can
 * update the value but it *hasn't changed the original*
 * version in the Main method. It will still remain at 10.
 */
public static void RegenerateHealth(HealthStruct healthStruct)
{
    healthStruct.Health++;
}

Namespace & Using

A namespace is a container for related classes and structs. They also enable you to reference code using a fully-qualified name.

  1. We first import the System library with the using keyword. This allows us to interact with basic functionality of the operating system, like being able to output text to the command line.
  2. Namespaces are the highest code containing structure and are used to contain classes or structs. We can refer to classes by accessing them via their namespace, e.g. System.Math.PI
using System; // [1]

namespace HelloWorld // [2]
{
    class Program
    {
        static void Main(string[] args) 
        {
            Console.WriteLine("Hello, {0}!", args[0].ToString());
        }
    }
}    

Value and Reference Types

All types are either value types or reference types, which may be assigned in memory to either the stack or the heap. Stacks and heaps refer to the ways in which memory is allocated. See: https://docs.microsoft.com/en-us/dotnet/csharp/write-safe-efficient-code

Value types

Booleans, DateTimes, chars etc. These are your local variables, method arguments and structs. All stack allocated. Value type variables hold instances with their own memory allocation. Only the value really matters, but the uniqueness of a particular object is not relevant. Generally “costs less” to use value types and so access time is faster.

Reference types

Classes, globals, strings, arrays, static fields: Heap allocated. Reference type variables hold references to instances. Reference types have a pointer to a memory location (the heap) containing the data, but not to the data itself. A Player can change its name or score, but it is still the same Player. Generally “costs more” to use reference types and so access time is slower.

Bonus: Install .NET Core on Linux

In Arch or Manjaro Linux, install the latest .NET Core framework with the following command.

pacman -S dotnet-runtime dotnet-sdk

You can opt out of Microsoft’s telemetry by appending this to your shell config file.

echo 'export DOTNET_CLI_TELEMETRY_OPTOUT=1' >> ~/.bashrc

This will install, among other things, the dotnet command line tool which is used to install .NET Core packages, run your development web server, and more.

Bonus: Install Entity Framework Core CLI tool

If you plan to work with databases, you may want the Entity Framework Core ORM. EFCore comes with its own tool called dotnet-ef for creating migrations and other ORM operations.

dotnet tool install --global dotnet-ef

I ran into a problem installing dotnet-ef. Running the tool only resulted in the following error message:

A fatal error occurred, the required library libhostfxr.so could not be found.
If this is a self-contained application, that library should exist in [/home/manjaro/.dotnet/tools/.store/dotnet-ef/2.2.4/dotnet-ef/2.2.4/tools/netcoreapp2.2/any/].
If this is a framework-dependent application, install the runtime in the default location [/usr/share/dotnet] or use the DOTNET_ROOT environment variable to specify the runtime location.

The DOTNET_ROOT part is the most straightforward solution. In your .bashrc add

export DOTNET_ROOT=/opt/dotnet

Execute source ~/.bashrc to reload settings and dotnet-ef should now work as expected.

Liked it? Take a second to support Eric on Patreon!
Default image
Eric
Code guy.
Leave a Reply