Breaking Down a Simple C# Program


C# demands that all program logic be contained within a type definition. Recall that a type is a general term referring to a member of the set {class, interface, structure, enumeration, delegate}. In C#, it is not possible to create global functions or global points of data. All data members and all methods must be contained within a type definition.

Prior to C# 10, you were required to write the following to print "Hello World" to the standard console output window:

using System;

namespace SimpleCSharpApp
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Hello World");
    }
  }
}

Now in C# 10, it can be done with one line of code:

Console.WriteLine("Hello World");

The Console class is contained in the System namespace. With the implicit global namespaces provided by .NET 6/C# 10, the using System; statement is no longer needed. Both the namespace and Program class can be removed due to the top-level statement functionality introduced in C# 9.

Note: all C# keywords are lowercase, while namespaces, types, and member names begin by convention with an initial capital letter and the first letter of any embedded words is capitalized.

class Program
{
  static void Main(string[] args)
  {
    // Display a simple message to the user
    Console.WriteLine("***** My First C# App *****");
    Console.WriteLine("Hello World");
    Console.WriteLine();

    // Wait for Enter to be pressed before shutting down
    Console.ReadLine();
  }
}

By default, the C# project templates that do not use top-level statements name the class containing the Main() method Program but you can change this if you want. Prior to C# 9, every executable C# application must contain a class defining a Main() method which signifies the entry point of the application.

Formally, the class that defines the Main() method is termed the application object. It is possible for a single executable application to have more than one application object. In this case, the compiler must known which Main() method should be used as the entry point. For now, also note that static members are scoped to the class level rather than the object level. This means that they can be invoked without the need to create a new instance of the class.

This Main() method also takes an array of strings which can contain any number of incoming command-line arguments. By not explicitly defining a return value, this Main() method has been setup to return a void return value.

Among the members of the Console class are WriteLine() and ReadLine(). WriteLine() sends a text string and carriage return to the standard output. Console.ReadLine() ensures that the command prompt launched by the Visual Studio IDE remains visible, but note that this would be the default anyway when running .NET Core Console apps with Visual Studio.

Using Top-Level Statements


Top-level statements which were introduced by C# 9.0 eliminate much of the ceremony around the C# application's entry point. There are some rules around top-level statements. Only one file in the app can use them. When using them, the program cannot have a declared entry point. They cannot be enclosed in a namespace. They can still access a string array of args. Top-level statements return an application code by using a return. Functions that would have been declared in the Program class become local functions for the top-level statements. The top-level statements compile for a class named Program. This allows for the addition of a partial Program class to hold regular methods. Additional types can be declared after all top-level statements. Trying to declare a type before the end of the top-level statements will cause a compilation error.

Specifying an Application Error Code


The vast majority of Main() methods will return void. However, C# is consistent with other C-based languages with the ability to return an int. By convention, returning a 0 indicates success. 0 is automatically returned even if the Main() method is prototyped to return void. A different value, such as -1, represents an error condition. When using top-level statements, if the executing code returns an integer, then that is the return code. If nothing is explicitly returned, it still returns 0.

The application's return value is passed to the system at the time the application terminates. A script can be used to run the app and capture the return value. Note that the vast majority of Main() will use void as the return value which implicitly returns the error code of 0.

Processing Command-Line Arguments


// Display a message and wait for Enter key to be pressed.
Console.WriteLine("My First C# App");
Console.WriteLine("Hello world");
Console.WriteLine();
// Process any incoming args.
for (int i = 0; i < args.Length; i++)
{
  Console.WriteLine("Arg: {0}", args[i]);
}
Console.ReadLine();
// Return an arbitrary error code.
return 0;

This example is using top-level statements. If the generated IL for the program is inspected, the <Main>$ method accepts a string array named args. If the program is using a Main() method as the entry point, then the method signature must accept a string array of args:

static int Main(string[] args)
{
...
}

As an alternative to the standard for loop, you can also iterate over an incoming string array using the foreach keyword.

foreach(string args in args)
{
  Console.WriteLine("Arg: {0}", arg);
}
Console.ReadLine();
return 0;

You are also able to access command-line arguments using the static GetCommandLineArgs() method of the System.Environment type. This returns an array of strings. The first entry will be the name of the application itself and the rest of the elements are the individual command-line arguments.

string[] theArgs = Environment.GetCommandLineArgs();
foreach(string arg in theArgs)
{
  Console.WriteLine("Arg: {0}", arg);
}
Console.ReadLine();
return 0;

Note that the GetCommandLineArgs method does not receive the arguments for the application through the Main() method and does not depend on the string[] args parameter.

Specifying Command-Line Arguments with Visual Studio 2022


Note you can do this for testing and debugging purposes in development.

Additional Members of the System.Environment Class


The Environment class exposes a number of extremely helpful methods beyond GetCommandLineArgs(). It lets you get details regarding the operating system currently hosting the app and other things.

Using the System.Console Class


The Console class encapsulates input, output, and error-stream manipulations for console-based applications.



Performing Basic Input and Output (I/O) with the Console Class


The Console type defines a set of methods to capture input and output. All of these are static and therefore prefixed by the name of the class. WriteLine() pumps a text string (including a carriage return) to the output system. Write() does this without a carriage return. ReadLine() allows you to receive information from the input stream up until the Enter key is pressed. Read() is used to capture a single character from the input stream.

static void GetUserData()
{
  Console.Write("Please enter your name: ");
  string userName = Console.ReadLine();
  Console.Write("Please enter your age: ");
  string userAge = Console.ReadLine();

  ConsoleColor prevColor = Console.ForegroundColor;
  Console.ForegroundColor = ConsoleColor.Yellow;

  Console.WriteLine("Hello {0}! You are {1} years old.",
  userName, userAge);

  Console.ForegroundColor = prevColor;
}

Formatting Console Output


.NET 6 supports a style of string formatting slightly similar to the printf() statement of C. When you are defining a string literal that contains segments of data whose value is not known until runtime, you are able to specify a placeholder within the string literal using the curly-bracket syntax. At runtime, the values passed into Console.WriteLine() are substituted for each placeholder. Note that if you have more uniquely numbered curly-bracket placeholders than fill arguments, you will get a format exception at runtime. But if there are too many fill arguments, the unused ones are ignored. You can also repeat the same placeholder in a given string. An alternative way to format strings is string interpolation.

Formatting Numerical Data




Console.WriteLine("c format: {0:c}", 99999);
Console.WriteLine(f3 format: {0:f3}", 99999);

Formatting Numerical Data Beyond Console Applications


The same formatting syntax can be used with the string.Format() method which returns a new String object.

string userMessage = string.Format("10000 in hex is {0:x}", 10000);

Working with System Data Types and Corresponding C# Keywords


C# defines keywords for fundamental data types which are used to represent local variables, class data member variables, method return values, and parameters. The C# data type keywords are shorthand notations for types in the System namespace. Not all of the types are compliant with the Common Language Specification.



Understanding Variable Declaration and Initialization


To declare a local variable, specify the type followed by the variable's name.

If you make use of a local variable before assigning an initial value, it will result in a compiler error. Thus it is a good practice to assign an initial value at the time of declaration. You can declare and initialize the variable on the same line or you can separate it into two code statements.

You can also declare multiple variables of the same type on one line:

bool b1 = true, b2 = false, b3 = b1;

The C# bool keyword is a shorthand notation for the System.Boolean structure. You can also declare variables using the full name of the type:

System.Boolean b4 = false;

The default Literal


default is a literal that assigns a default value for the data type:

int myInt = default;

Using Intrinsic Data Types and the new Operator


All intrinsic data types support what is known as the default constructor. This lets you create a variable using the new keyword. The new keyword automatically sets the variable to its default value.

bool variables are set to false. Numeric data is set to 0 or 0.0 if it is a floating-point type. char variables are set to a single empty character. BigInteger variables are set to 0. Object references (including strings) are set to null.

bool b = new bool();

C# 9.0 added a shortcut for creating variable instances:

bool b = new();

Understanding the Data Type Class Hierarchy


Even the primitive .NET data types are arranged in a class hierarchy.



Ultimately, each type if derived from System.Object. This defines a set of methods common to all types in the .NET Core base class libraries including ToString(), Equals(), and GetHashCode().

Note that many numerical types derive from a class called ValueType. Descendents of ValueType are automatically allocated on the stack, and therefore, have a predictable lifetime and are quite efficient. Types that do not have System.ValueType are instead allocated on the garbage-collected heap.

Understanding the Members of Numerical Data Types


The numerical types of .NET Core support MaxValue and MinValue properties that provide the range a given type can store. Some types also have other members.

Understanding the Members of System.Boolean


The only valid assignment a C# bool can take is from the set true and false. It supports TrueString and FalseString properties.

Understanding the Members of System.Char


Both System.String and System.Char are Unicode under the hood.

Parsing Values from String Data


A form of parsing is generating a variable of a type given a textual equivalent and is supported by the .NET data types.

bool b = bool.Parse("True");

Using TryParse to Parse Values from String Data


Note that this will fail at runtime:

bool b = bool.Parse("Hello");

It can be wrapped in a try-catch block or a TryParse() statement. TryParse() takes an out parameter and returns a bool if the parsing was successful.

Using System.DateTime and System.TimeSpan


The System namespace also defines a few useful types that do not have C# keywords. For example, DateTime which contains data that represents a specific date and time value and TimeSpan which allows you to easily define and transform units of time using various members.

Working with the System.Numerics Namespace


This namespace defines a structure called BigInteger. It also has a structure called Complex.

If you need to define a massive numerical value, the first step is to add the following using directive:

using System.Numerics;

Note a BigInteger variable cannot be changed as the data is immutable.

Using Digit Separators


The _ was introduced as a digit separator in C# 7.0 for integer, long, decimal, double data, and hex types.

Console.Write(123_456.12);

Using Binary Literals


C# 7.0 also introduced a new literal for binary values that could be used to create bit masks for example.

Console.WriteLine(0b_0001_0000);

Working with String Data




Be aware that a few members of System.String are static and therefore called at the class level.

Note strings are immutable and so methods like Replace() actually are returning a new string.

Performing String Concatenation


string s3 = s1 + s2;

The C# + symbol here is processed by the compiler to emit a call to the static String.Concat() method.

string s3 = String.Concat(s1, s2);

Using Escape Characters


C# string literals may contain various escape characters which qualify how the character data should be printed to the output stream. Each begins with a backslash.



Performing String Interpolation


String interpolation, using string literals that contain placeholders for variables, was introduced by C# 6.0.

Performance Improvements


String handling has improved performance in C# 10 when your code contains string interpolation.

Defining Verbatim Strings


A string prefixed with the @ symbol is what is known as a verbatim string. This disables the processing of the literal's escape characters.

Working with Strings and Equality


A reference type is an object allocated on the garbage-collected managed heap. By default, when you perform a test for equality on reference types (== and !=) it will return true if the references are pointing to the same object in memory. Even though the string data type is a reference type, the equality operators were redefined to compare the values of the string objects.

Modifying String Comparison Behavior




Strings Are Immutable


After a string object is assigned a value, the value cannot be changed. Methods of the string type simply return a new string. The string class can be inefficient and result in bloated code if misused especially when performing string concatenation or working with huge amounts of text data.

Using the System.Text.StringBuilder Typer


The .NET Core base class libraries provide the System.Text namespace to solve some of the performance issues of the string type when it is used with reckless abandon.

using System.Text;

Constructors allow you to create an object with an initial state when you apply the new keyword and there are several that create instances of StringBuilder. Members of the StringBuilder type modify the object's internal character data which makes it more efficient.

StringBuilder sb = new StringBuild("*****");
sb.Append("\n");

Narrowing and Widening Data Type Conversions


short numb1 = 9, numb2 = 10;
Console.WriteLine("{0} + {1}",
  numb1, numb2, Add(numb1, numb2));
Console.ReadLine();

static int Add(int x, int y)
{
  return x + y;
}

Add() expects two int arguments but it was called with two short variables. The program compiles and executres without error because there is no possibility for loss of data. The maximum range of a short is well within the maximum range of an int. The compiler implicitly widens each short to an int.

Formally speaking, widening is the term used to define an implicit upward cast that does not result in a loss of data.

 Narrowing is the opposite. All narrowing converstions result in a compiler error. If you want to tell the compiler that you are willing to deal with a possible loss of data because of a narrowing operation, you must apply an explicit cast using the C# casting operator, ().

short answer = (short)Add(numb1, numb2);

An explicit cast allows you to force the compiler to apply a narrowing conversion and may result in an incorrect value.

Using the checked Keyword


The checked keyword can wrap a statement or a block of statements. Within the scope of checked, additional CIL instructions will test for overflow conditions. If an overflow occurs, it will result in a runtime exception: System.OverflowException.

byte sum = checked((byte)Add(b1, b2));

Setting Project-wide Overflow Checking


This can be set for the entire project with configuration of the project file. All arithmetic can be evaluated for overflow without the need to use the checked keyword. It can also be configured through Visual Studio.

Using the unchecked Keyword


The unchecked keyword can be used if you want to have a block of code where data loss is acceptable after you have done the above.

Understanding Implicitly Typed Local Variables


Many would argue it is generally a good practice to explicitly specify the data type of every variable. The C# language does however provide for implicitly typing using the var keyword. The var keyword can be used in place of specifying a data type. The compiler will automatically infer the underlying data type based on the initial value used to initialize the data point. You can verify that the types have been set correctly using reflection, which is the act of determining the composition of a type at runtime.

Console.WriteLine("myString is a: {0}", myString.GetType().Name);

Note you can use implicit typing for any type.

Declaring Numerics Implicitly


Whole numbers default to integers. Floating-point numbers default to doubles.

Understanding Restrictions on Implicitly Typed Variables


There are some restrictions to using var. First, implicit typing applies only to local variables in a method or property scope. It is illegal to use var to define return values, parameters, or field data of a custom type.

class ThisWillNeverCompile
{
  // Error!
  private var myInt = 10;

  // Error!
  public var MyMethod(var x, var y){}
}

Also, local variables declared using var must be assigned an initial value. They cannot be assigned the initial value of null. It is permissible to assign an inferred local variable to null after its initial assignment provided it is a reference type.

Implicitly Typed Data is Strongly Typed Data


Be aware that implicit typing of local variables results in strongly typed data. Therefore the use of var is not the same technique when used with scripting languages (Javascript, Perl) or the COM Variant data type, where a variable can hold values of different types over its lifetime in a program (called dynamic typing). Note C# does allow for dynamic typing through the dynamic keyword.

Understanding the Usefulness of Implicitly Typed Local Variables


Using var to declare local variables simply for the sake of doing so brings little to the table. It can make reading the code more confusing as it becomes harder to quickly determine the underlying data type. If you know you need an int, declare an int!

The LINQ technology set makes use of query expressions that can yield dynamically created result sets based on the format of the query itself. Implicit typing is extremely helpful in these cases as you do not need to explicitly define the type that a query may return, which in some cases would be impossible to do. When you are using LINQ, you seldom if ever care about the underlying type of the query's return value. It could be argued that the only time you would make use of the var keyword is when defining data returned from a LINQ query. Overuse of implicit typing (via the var keyword) is considered by most developers to be poor style in production code.

Working with C# Iteration Constructs


C# provides these iteration constructs: for loop, foreach/in loop, while loop, do/while loop.

for(int i = 0; i < 4; i++)
{
  Console.WriteLine("Number is: {0} ", i);
}


string[] carTypes = {"Ford", "BMW", "Yugo", "Honda"};
foreach (string c in carTypes)
{
  Console.WriteLine(c);
}

// LINQ query!
var subset = from i in numbers where i < 10 select i;

foreach (var i in subset)
{
  Console.Write("{0} ", i);
}

while(userIsDown.ToLower() != "yes")
{
...
}


do/while are similar to while loops but are guaranteed to execute the corresponding block of code at least once.

A Quick Discussion About Scope


A scope is created uysing curly braces. It is allowed to not use curly braces, but typically it is not a good idea.

Working with Decision Constructs and the Relational/Equality Operators


C# defines two simple constructs to alter the flow of your program: if/else and switch.

Unlike in C and C++, the if/else statement in C# operates only on Boolean expressions.

New in C# 7.0, pattern matching is allowed in if/else statements.

The conditional operator (?:) also known as the ternary conditional operator is a shorthand of writing a simple if/else statement. There are some limitations: both expressions must have an implicit conversion to a target type, and it can only be used in assignment statements.

Using Logical Operators


&&, ||, !

Note that && and || short-circuit. & and | can be used instead if you require all expressions to be tested regardless.

Using the switch Statement


The switch statement can evaluate many different types. If multiple case statements should produce the same result, they can be combined. The switch statement also supports goto as a way to exit a case condition, but it should not be used generally. The switch can also be done on the type of a variable being checked.

Using switch Expressions


static string FromRainbow(string colorBand)
{
  return colorBand switch
  {
    "Red" => "FF0000",
    "Orange" => "FF7F00",
    _ => "#FFFFFF",
  };
}

Tuples can be used to compare more than one value during a switch statement:

static string RockPaperScissors(string first, string second)
{
  return (first, second) switch
  {
    ("rock", "paper") => "Paper wins.",
    ...
    (_, _) => "Tie.",
  };
}

Summary


The goal of this chapter was just to expose you to numerous aspects of the C# programming language. Every C# executable program must have a type defining a Main() method either explicitly or implicitly through the use of top-level statements. Each data type keyword (e.g. int) is really a shorthand notation for a full-blown type in the System namespace. Given this, each C# data type has a number of built-in members.

The most useful place for implicit typing (using var) is when working with the LINQ programming model.

The next chapter completes the examination of core language features. After that will be the object-oriented features.

Article notes

What in C# is a general term referring to a member of the set {class, interface, structure, enumeration, delegate}?
C# demands that all program logic be contained within a what?
What is missing from this list of things that are each considered a type in C#? interface, structure, enumeration, delegate
What is missing from this list of things that are each considered a type in C#? class, structure, enumeration, delegate
What is missing from this list of things that are each considered a type in C#? class, interface, enumeration, delegate
What is missing from this list of things that are each considered a type in C#? class, interface, structure, delegate
What is missing from this list of things that are each considered a type in C#? class, interface, structure, enumeration
How do you create a global function or point of data in C#?
What is the namespace that contains the C# Console class?
using System; What feature made it so that this statement is no longer needed to write a simple C# hello world program?
What using statement is no longer needed to write a hello world program in C# with the new implicit global namespaces?
What is an example of iterating through a string array (called args) using the foreach keyword in C#?
What is an example (C#) of iterating through a string array (called args) using a standard for loop?
What is the case convention for all C# keywords?
What is the case convention for all namespaces, types, and member names in C#?
What relatively new feature of C# can be used to eliminate much of the ceremony around the C# application's entry point?
When not using top-level statements, what conventionally is the name for the class that includes the Main() method in a C# program?
What method signifies the entry point of a C# application, and prior to C# 9 which introduced the top-level statement functionality, was required in every C# app?
What term refers to the class that defines the Main() method in a C# app?
What member of the C# Console class sends a text string and a carriage return to the standard output?
How many files in a C# app are allowed to use top-level statements?
What can a C# Main() method do (if you want) to specify an application error code (or code that indicates success)?
By convention, what integer returned by an app indicates success?
What is the C# Console method which is similar to WriteLine() but does not append a carriage return to the end of the text string it outputs?
What type in C# defines a set of static methods to do basic I/O?
What is the C# Console method which allows you to receive information from the input stream up until the Enter key is pressed?
What is the C# Console method that can be used to capture a single character from the input stream?
Does using the var keyword in C# result in dynamic typing?
The C# Console methods that capture input and output are all called by being prefixed with the name of the class because they are what type of methods?
What are the 2 steps to declare a local variable in C#?
At one point is it a good practice to assign an initial value to a declared variable in a language like C#?
What C# keyword is a shorthand notation for System.Int32?
How many bits is an int in C#?
How many bits is a uint in C#?
How many bits is a short in C#?
How many bits is a ushort in C#?
How many bits is a long in C#?
What C# keyword is a shorthand notation for System.Int16?
What is the highest value that a variable of type ushort can have in C#?
What is the lowest value that a variable of type sbyte can have in C#?
What is the highest value that a variable of type sbyte can have in C#?
What is the highest value that a variable of type byte can have in C#?
What C# keyword is a shorthand notation for System.Boolean?
What type in the C# System namespace has the shorthand notation float?
What type in the C# System namespace has the shorthand notation double?
What C# literal can be used to assign a default value for the data type?
What is the keyword used in C# when declaring a variable using the default constructor?
What is the ultimate base class of all C# types?
Can a class derive from ValueType in C#?
What creates a scope in C#?
What class in C# defines a set of methods common to all types in the .NET Core base class libraries including ToString(), Equals(), and GetHashCode()?
Where are types that descend from ValueType (of which many numerical types derive from) allocated?
What word describes generating a variable of a type given a textual equivalent?
What can be used as a digit separator character in C#?
What is an example of what you might want to do using literal binary values like 0b_0001_0000 in C#?
What is the property in C# that returns the length of the current string?
What are the characters in C# called that when present in string literals, qualify how the character data should be printed to the output stream?
What is the escape character in C# that inserts a double quote into a string literal?
What is the escape character in C# that inserts a single quote into a string literal?
What is the escape character in C# that inserts a backslash into a string literal?
What is the escape character in C# that inserts a horizontal tab into a string literal?
What programming feature involves a string literal containing placeholders for values?
What is a string that is prefixed with the @ symbol in C# known as?
What character can a string be prefixed with in C# that will disable the processing of the string literal's escape characters?
Where are reference types allocated in C#?
When a test for equality on reference types is performed in C# (== and !=) what is the condition for == to return true?
Since methods called on strings in C# return new strings, we can say that strings are what?
What properties of numerical types in .NET Core can be used to determine the range a given type can store?
What is the C# namespace that defines BigInteger?
What is the C# namespace that defines Complex?
What refers to an implicit upward cast that does not result in a loss of data (e.g. Add() expects two int arguments but it can be called with two shorts because there is no possibility for data loss)?
What is the property in C# that returns the length of the current string?
What method in C# is analogous to upcase in Ruby?
What method in C# is analogous to downcase in Ruby?
What is the C# casting operator?
What is the act of determining the composition of a type at runtime?
What does using var to declare local variables in C# simply for the sake of doing so bring to the table?
It might be argued that the only time to use the var keyword in C# is when?
How is a scope created in C#?
Do && and || short-circuit in C#?
Do & and | short-circuit in C#?
What language construct/keyword in C# and other C-based languages allows you to handle program flow based on a predefined set of choices?
What statement in C# takes a string and an out parameter, parses the string and assigns the parsed value to the out variable, and returns true if successful?
An object allocated on the garbage-collected managed heap in C# is what kind of type in general?
How is a scope created in C, C#, Java, and other languages based on C?
When using the C# operators like == and != with reference types, when would a test for equality return true?
What enum in C# has values which can be used by overloads of methods like Equals() and Compare() to control how the comparisons are done with respect to things like culture?
What can we say about strings in C# that describes this: once a string object is assigned an initial value, the character data cannot be changed?
Previous Next