Skip to content

Basic Syntax

Basic Setup

Tooling

dotnet commands

  • dotnet new console -n LearnCSharp - create a new project called LearnCSharp. console is fine for C#. Later, this can be replaced to create ASP.NET, Blazor, Maui applications.
  • dotnet build - build an executable.
  • dotnet run - build and run the executable (use this during development).

Folder structure

  • LearnCSharp.csproj - Used by .NET to get information on how to build the project (can ignore).
  • LearnCSharp.sln - Specific to Visual Studio (Code) and is used for organizing projects. Multiple projects can be maintained together by using a solution file. (can ignore). docs
  • Program.cs - Entry point for dotnet run.

VSCode specific

  • In bottom-left expand Solution Explorer. Reads LearnCSharp.sln file, to create this view. docs

Main method (entry point)

  1. Hello World example

    Program.cs
    1
    namespace LearnCSharp
    2
    {
    3
    class Program
    4
    {
    5
    static void Main(string[] args)
    6
    {
    7
    Console.WriteLine("Hello, World!");
    8
    }
    9
    }
    10
    }
  2. By default LearnCSharp namespace is created. So namespace LearnCSharp can be skipped.

    Program.cs
    1
    class MyClass
    2
    {
    3
    static void Main(string[] args)
    4
    {
    5
    Console.WriteLine("Hello, World!");
    6
    }
    7
    }

    Namespace details

    • Option 1. Change default namespace name. Specify the name of new namespace in LearnCSharp.csproj as <RootNamespace>YourNameSpace</RootNamespace>

      Program.cs
      1
      namespace YourNameSpace
      2
      {
      3
      class MyClass
      4
      {
      5
      static void Main(string[] args)
      6
      {
      7
      Console.WriteLine("Hello, World!");
      8
      }
      9
      }
      10
      }
      LearnCSharp.csproj
      1
      <Project Sdk="Microsoft.NET.Sdk">
      2
      3
      <PropertyGroup>
      4
      <OutputType>Exe</OutputType>
      5
      <TargetFramework>net9.0</TargetFramework>
      6
      <ImplicitUsings>enable</ImplicitUsings>
      7
      <Nullable>enable</Nullable>
      8
      <RootNameSpace>YourNameSpace</RootNameSpace>
      9
      </PropertyGroup>
      10
      11
      </Project>
    • Option 2. Although adding <RootNameSpace> to LearnCSharp.csproj can be skipped. This is not recommended, since this breaks C# tooling. C# tooling expects namespace to be LearnCSharp.FolderName (as shown in Option 3).

    • Option 3. Create YourNameSpace folder with temp.cs. The namespace can now be changed to LearnCSharp.YourNameSpace.

      YourNameSpace/temp.cs
      1
      namespace LearnCSharp.YourNameSpace
      2
      {
      3
      class Temp
      4
      {
      5
      public static void Function()
      6
      {
      7
      Console.WriteLine("From another namespace");
      8
      }
      9
      }
      10
      }

      Call the Function from Main

      Program.cs
      1
      namespace LearnCSharp
      2
      {
      3
      class MyClass
      4
      {
      5
      static void Main()
      6
      {
      7
      LearnCSharp.YourNameSpace.Temp.Function();
      8
      }
      9
      }
      10
      }

    Namespaces are used to organize and group related classes, primarily to avoid name conflicts when multiple classes with the same name exist in different parts of the project. Unlike Node.js, where functions or modules must be explicitly imported to be used in the current file, C# allows accessing classes across files without explicit import statements as long as they share the same namespace.

    Example of calling a class from another file, without any import statements

    Subfolder/temp.cs
    1
    class Temp
    2
    {
    3
    public static void Function()
    4
    {
    5
    Console.WriteLine("From another namespace");
    6
    }
    7
    }
    Program.cs
    1
    class MyClass
    2
    {
    3
    static void Main()
    4
    {
    5
    Temp.Function();
    6
    }
    7
    }
  3. Main method details

    • Entry point for C# application. Can be defined in a class or struct.
    • Can have any access modifier(public, private, protected, internal, protected internal, private protected) except file.
    • Must be static.
    • Return type must be void, int, Task, Task<int>.
    • Only one class can define Main method. In case multiple classes define Main, the program needs to be compiled with StartupObject option to specify which Main method to use as the entry point. docs

    Specifying args in Main

    • string[] args is 0-indexed and does not include the file name of the program.

      Program.cs
      1
      class Program
      2
      {
      3
      public static void Main(string[] args)
      4
      {
      5
      string arg1 = args[0];
      6
      string arg2 = args[1];
      7
      8
      Console.WriteLine(arg1);
      9
      Console.WriteLine(arg2);
      10
      }
      11
      }
      dotnet run 1 2
      1
      1
      2
      2
    • Named argument example using string[] args

      Program.cs
      1
      class Program
      2
      {
      3
      public static void Main(string[] args)
      4
      {
      5
      string[] arg1 = args[0].Split('=');
      6
      string key = arg1[0];
      7
      string value = arg1[1];
      8
      9
      Console.WriteLine($"{key}: {value}");
      10
      }
      11
      }
      dotnet run name=Kushaj
      1
      name: Kushaj
    • Environment.GetCommandLineArgs() is an alternative. This includes the file name of the program at 0-index as well.

      Program.cs
      1
      class Program
      2
      {
      3
      public static void Main()
      4
      {
      5
      string[] args = Environment.GetCommandLineArgs();
      6
      7
      Console.WriteLine(args[0]);
      8
      Console.WriteLine(args[1]);
      9
      Console.WriteLine(args[2]);
      10
      }
      11
      }
      dotnet run 1 name=Kushaj
      1
      C:\Users\ks56866\Desktop\files\LearnCSharp\bin\Debug\net9.0\LearnCSharp.dll
      2
      1
      3
      name=Kushaj
    • Do not parse args manually. Use System.CommandLine. github

    Providing a return statement to Main. This is used to communicate if the program executed successfully.

    • In Windows, the return value of Main is stored in an environment variable, which can be retrieved using ERRORLEVEL from .bat file or $LastExitCode from .ps1 file.

      Program.cs
      1
      class Program
      2
      {
      3
      private static int Main()
      4
      {
      5
      Console.WriteLine("Hello, World!");
      6
      return 0;
      7
      }
      8
      }

      Create Powershell file to execute the program

      temp.ps1
      1
      dotnet run
      2
      3
      if ($LastExitCode -eq 0) {
      4
      Write-Host "Execution succeeded"
      5
      } else {
      6
      Write-Host "Execution Failed"
      7
      }
      8
      9
      Write-Host "Return value = " $LastExitCode

      Execute powershell file

      ./temp.ps1
      1
      Hello, World!
      2
      Execution succeeded
      3
      Return value = 0
    • In Linux, $? holds the value of the exit code of the last executed command. And the equivalent to the powershell file is

      temp.sh
      1
      #!/bin/bash
      2
      3
      dotnet run
      4
      5
      if [ $? -eq 0 ]; then
      6
      echo "Execution succeeded"
      7
      else
      8
      echo "Execution failed"
      9
      fi
      10
      11
      echo "Return value = $?"
  4. Top-level statements. An alternative to Main. Do not use this.

    Program.cs
    1
    Console.WriteLine("Hello World!");

    The above is equivalent to

    Program.cs
    1
    internal class Program
    2
    {
    3
    private static void Main(string[] args)
    4
    {
    5
    Console.WriteLine("Hello World!");
    6
    }
    7
    }

General structure of C# Program

Program.cs
1
using System;
2
3
namespace LearnCSharp
4
{
5
class YourClass { }
6
7
struct YourStruct { }
8
9
interface IYourInterface { }
10
11
delegate int YourDelegate();
12
13
enum YourEnum { }
14
15
namespace YourNestedNamespace
16
{
17
18
}
19
20
class Program
21
{
22
static void Main(string[] args)
23
{
24
Console.WriteLine("Hello World!");
25
}
26
}
27
}

Type system

In addition to the compiler checking type safety at compile time, the compiler embeds the type information into the executable file as metadata. The common language runtime (CLR) uses that metadata at runtime to further gurantee type safety when it allocates and reclaims memory.

All types derive from the base type System.Object. This unified type hierarchy is called Common Type System (CTS). In CTS a type can be either a value type or reference type.

  • All built-in types are struct and are called value types.
    • Value types are sealed, meaning you can’t derive a type from any value type.
    • The memory is allocated inline in whatever context the variable is declared, and there is no separate heap allocation or garbage collection overhead.
    • Value types are of two types struct and enum.
    • struct is used to create custom value types.
  • Types defined using class or record are reference types.
    • The default value at initalization is null.
    • When object is created, the memory is allocated on the managed heap, and the variable holds reference to the location of the object.
    • In the garbage collection process, there is overhead for both allocation and deallocation.
  • Literal values. For example 4.56. These automatically receive a type from the compiler, which inherits from System.Object as well.
    • The type can be specified to the compiler using 4.56f.
  • Generic types. For example System.Collections.Generic.List<T>. T here is the placeholder for the actual type (the concrete type).
  • var can be used to provide an implicit type. The variable would receive the type at compile time.
  • Add ? at the end of type (nullable value types) to allow the value to be null.
    • Inherit from System.Nullable<T>.

If you define a class, struct, or record named Person, Person is the name of the type. If you declare and initialize a variable p of type Person, p is said to be an object or instance of Person.

In some situations, compile-time and run-time types can be different.

1
object anotherMessage = "This is another string of characters";
2
IEnumerable<char> someCharacters = "abcdefghijklmnopqrstuvwxyz";

In the above example, the run-time type is string but the compile-time type is object and IEnumerable<char>.

  • Compile-time type determines all the actions taken by the compiler, like method call resolution, overload resolution, and available implicit and explicit casts.
  • Run-time type determines all actions that are resolved at run time, like dispatching virtual method calls, evaluating is and switch expressions, and other type testing APIs.

Custom types

How to choose which one to use

  • If the data storage size is small, no more than 64 bytes, choose struct or record struct.
  • If the type is immutable, or you want nondestructive mutation (create a new instance of object with modified values, instead of directly modifying the original instance), choose struct or record struct.
  • If your type should have value semantics for equality (two instances are considered equal if their values are the same), choose record class or record struct.
  • If the type is primarily used for storing data, not behavior, choose a record class or record struct.
  • If the type is part of an inheritance hierarchy, choose a record class or class.
  • If the type uses polymorphism, choose a class.
  • If the primary purpose if behavior, choose a class.

Summary of above

  • Classes typpically store data that is intended to be modifed after a class object is created.
  • Structs are best suited for small data structures, that typically would not be modifed after the struct is created.
  • Records typpically store data that isn’t instended to be modified after object is created. And they are mostly used for checking value equality, since records modify Equals and GetHashCode, and two records are equal if all their properties have the same values.

Namespaces

Namespaces used to organize classes. System is namespace and Console is class inside that namespace.

1
System.Console.WriteLine("hello world");

To avoid writing the namespace use using

1
using System;
2
Console.WriteLine("hello, world");

global is the “root” namespace. This is set by default by .NET depending on the application.

1
global::System.Console.WriteLine("hello");

Classes

  • When a variable is declared with class, the default value is null until an onject is assigned.
  • When the object is created, enough memory is allocated on the managed heap for that specific object, which is later reclaimed by garbage collector.

Class defition [access modifier] - [class] - [identifier]. Example public class Customer

  • access modifier - default is internal.
  • all the fields, properties, methods and events defined in the class are collectively referred to as class members.

To create an object from the class use new. Example Customer object1 = new Customer(). This syntax can be simplified to Customer object1 = new ();

Use field initializer to set the default value of the fields in the class. And then in the constructor, you can provide an initial value as well.

1
public class Container
2
{
3
private int _capacity = 10; // field initializer
4
5
public Container(int capacity)
6
{
7
_capacity = capacity;
8
}
9
}

In the above example, since the constructor is only being used to set the values for the fields, you can use a primary constructor instead.

1
public class Container(int capacity)
2
{
3
private int _capacity = capacity;
4
}

Inheritance example public class Manager : Employee { }.

Records

Use records when

  • Object is immutable. Useful to make a type thread-safe, or you want the objects to have the same hash code in a hash table..
  • You are interested in checking equality (all property and fields values compare equal) between two objects.

Do not use record types, when you want to check for only one instance of a class. In that case, you need to do reference equality.

At compile time will create method for equality, ToString and Deconstruct (if positional parameters were used).

Syntax

  • Instead of class use record.
  • Instead of struct use record struct.

Example

1
public record Person(string FirstName, string LastName);
2
3
Person person1 = new("Kushajveer", "Singh");
4
Person person2 = new("Kushajveer", "Singh");
5
Console.WriteLine(person1 == person2); // True
6
7
// Use "with" to copy the object and change any properties
8
Person person3 = person1 with { FirstName = "Kushaj" };
9
Console.WriteLine(person1 == person3); // False

Interfaces