This chapter is all about the basics of the C# programming language. Over the course of this chapter, you’ll learn how to write statements using the grammar of C#, as well as being introduced to some of the common vocabulary that you will use every day. In addition to this, by the end of the chapter, you’ll feel confident in knowing how to temporarily store and work with information in your computer’s memory.
This chapter covers the following topics:
This part of the book is about the C# language—the grammar and vocabulary that you will use every day to write the source code for your applications.
Programming languages have many similarities to human languages, except that in programming languages, you can make up your own words, just like Dr. Seuss!
In a book written by Dr. Seuss in 1950, If I Ran the Zoo, he states this:
“And then, just to show them, I’ll sail to Ka-Troo And Bring Back an It-Kutch, a Preep, and a Proo, A Nerkle, a Nerd, and a Seersucker, too!”
This part of the book covers the C# programming language and is written primarily for beginners, so it covers the fundamental topics that all developers need to know, including declaring variables, storing data, and how to define your own custom data types.
This book covers features of the C# language from version 1 up to the latest version, 12.
If you already have some familiarity with older versions of C# and are excited to find out about the new features in the most recent versions of C#, I have made it easier for you to jump around by listing language versions and their important new features below, along with the chapter number and topic title where you can learn about them.
You can read this information in the GitHub repository at the following link: https://github.com/markjprice/cs12dotnet8/blob/main/docs/ch02-features.md
Over the years, Microsoft has submitted a few versions of C# to standards bodies, as shown in Table 2.1:
C# version |
ECMA standard |
ISO/IEC standard |
1.0 |
ECMA-334:2003 |
ISO/IEC 23270:2003 |
2.0 |
ECMA-334:2006 |
ISO/IEC 23270:2006 |
5.0 |
ECMA-334:2017 |
ISO/IEC 23270:2018 |
6.0 |
ECMA-334:2022 |
ISO/IEC 23270:2022 |
Table 2.1: ECMA standards for C#
The ECMA standard for C# 7.3 is still a draft. So don’t even think about when C# versions 8 to 12 might be ECMA standards! Microsoft made C# open source in 2014. You can read the latest C# standard document at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/specifications.
More practically useful than the ECMA standards are the public GitHub repositories for making the work on C# and related technologies as open as possible, as shown in Table 2.2:
Description |
Link |
C# language design |
|
Compiler implementation |
|
Standard to describe the language |
Table 2.2: Public GitHub repositories for C#
The .NET language compiler for C# and Visual Basic, also known as Roslyn, along with a separate compiler for F#, is distributed as part of the .NET SDK. To use a specific version of C#, you must have at least that version of the .NET SDK installed, as shown in Table 2.3:
.NET SDK |
Roslyn compiler |
Default C# language |
1.0.4 |
2.0-2.2 |
7.0 |
1.1.4 |
2.3-2.4 |
7.1 |
2.1.2 |
2.6-2.7 |
7.2 |
2.1.200 |
2.8-2.10 |
7.3 |
3.0 |
3.0-3.4 |
8.0 |
5.0 |
3.8 |
9.0 |
6.0 |
4.0 |
10.0 |
7.0 |
4.4 |
11.0 |
8.0 |
4.8 |
12.0 |
Table 2.3: .NET SDK versions and their C# compiler versions
When you create class libraries, you can choose to target .NET Standard as well as versions of modern .NET. They have default C# language versions, as shown in Table 2.4:
.NET Standard |
C# |
2.0 |
7.3 |
2.1 |
8.0 |
Table 2.4: .NET Standard versions and their default C# compiler versions
Although you must have a minimum version of the .NET SDK installed to have access to a specific compiler version, the projects that you create can target older versions of .NET and still use a modern compiler version. For example, if you have the .NET 7 SDK or later installed, then you can use C# 11 language features in a console app that targets .NET Core 3.0.
Let’s see what .NET SDK and C# language compiler versions you have available:
dotnet --version
8.0.100
Developer tools like Visual Studio and the dotnet
command-line interface assume that you want to use the latest major version of a C# language compiler by default. Before C# 8 was released, C# 7 was the latest major version and was used by default.
To use the improvements in a C# point release like 7.1, 7.2, or 7.3, you had to add a <LangVersion>
configuration element to the project file, as shown in the following markup:
<LangVersion>7.3</LangVersion>
After the release of C# 12 with .NET 8, if Microsoft releases a C# 12.1 compiler and you want to use its new language features, then you will have to add a configuration element to your project file, as shown in the following markup:
<LangVersion>12.1</LangVersion>
Potential values for the <LangVersion>
are shown in Table 2.5:
<LangVersion> |
Description |
|
Entering a specific version number will use that compiler if it has been installed. |
|
Uses the highest major number, for example, 7.0 in August 2019, 8 in October 2019, 9 in November 2020, 10 in November 2021, 11 in November 2022, and 12 in November 2023. |
|
Uses the highest major and highest minor number, for example, 7.2 in 2017, 7.3 in 2018, 8 in 2019, and perhaps 12.1 in H1 2024. |
|
Uses the highest available preview version, for example, 12.0 in July 2023 with .NET 8 Preview 6 installed. |
Table 2.5: LangVersion settings for a project file
In February 2024, Microsoft is likely to release the first preview of .NET 9 with a C# 13 compiler. You will be able to install its SDK from the following link:
https://dotnet.microsoft.com/en-us/download/dotnet/9.0
The link will give a 404 Missing resource
error until February 2024, so do not bother using it until then!
After you’ve installed a .NET 9 SDK preview, you will be able to use it to create new projects and explore the new language features in C# 13. After creating a new project, you can edit the .csproj
file and add the <LangVersion>
element set to preview
to use the preview C# 13 compiler, as shown highlighted in the following markup:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>preview</LangVersion>
</PropertyGroup>
</Project>
.NET 8 is an LTS release, so Microsoft must support developers who continue to use .NET 8 for three years. But that does not mean that you are stuck with the C# 12 compiler for three years!
In November 2024, Microsoft is likely to release .NET 9, including a C# 13 compiler with new features. Although future versions of .NET 8 are likely to include preview versions of the C# 13 compiler, to be properly supported by Microsoft, you should only set <LangVersion>
to preview
for exploration, not production projects, because it is not supported by Microsoft, and it is more likely to have bugs. Microsoft makes previews available because they want to hear feedback. You can be a part of C#’s development and improvement.
Once the .NET 9 SDK is made generally available in November 2024, you will be able to get the best of both worlds. You can use the .NET 9 SDK and its C# 13 compiler while your projects continue to target .NET 8. To do so, set the target framework to net8.0
and add a <LangVersion>
element set to 13
, as shown highlighted in the following markup:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>13</LangVersion>
</PropertyGroup>
</Project>
The preceding project targets net8.0
, so it is supported until November 2026 when run on a monthly patched version of the .NET 8 runtime. If the preceding project is built using .NET 9 SDK, then it can have the <LangVersion>
set to 13
, meaning C# 13.
If you target net9.0
, which new projects will by default if you have installed the .NET 9 SDK, then the default language will be C# 13 so it would not need to be explicitly set.
In February 2025, Microsoft is likely to release the first preview of .NET 10, and, in November 2025, it will likely release .NET 10 for general availability in production. You will be able to install its SDK from the following link and explore C# 14 in the same way as described above for C# 13 with .NET 9:
https://dotnet.microsoft.com/en-us/download/dotnet/10.0
Again, the preceding link is for future use! It will give a 404 Missing resource
error until February 2025, so do not bother using it until then.
Warning! Some C# language features depend on changes in the underlying .NET libraries. Even if you use the latest SDK with the latest compiler, you might not be able to use all the new language features while targeting an older version of .NET. For example, C# 11 introduced the required
keyword, but it cannot be used in a project that targets .NET 6 because that language feature requires new attributes that are only available in .NET 7. Luckily, the compiler will warn you if you try to use a C# feature that is not supported. Just be prepared for that eventuality.
We will start by writing code that shows the compiler version:
cs12dotnet8
folder. If not, then you’ll need to create it.console
Vocabulary
Chapter02
Good Practice: If you have forgotten how, or did not complete the previous chapter, then step-by-step instructions for creating a solution with multiple projects are given in Chapter 1, Hello, C#! Welcome, .NET!.
Vocabulary
project, in Program.cs
, after the comment, add a statement to show the C# version as an error, as shown in the following code:
#error version
Vocabulary
folder, enter the dotnet run
command. Note that we are expecting a compiler error, so do not panic when you see it!CS8304
, as shown in Figure 2.1:
Figure 2.1: A compiler error that shows the C# language version
The error message in the Visual Studio Code PROBLEMS window or Visual Studio Error List window says Compiler version: '4.8.0...'
with language version default (12.0)
.
// #error version
Let’s start by looking at the basics of the grammar and vocabulary of C#. Throughout this chapter, you will create multiple console apps, with each one showing related features of the C# language.
The grammar of C# includes statements and blocks. To document your code, you can use comments.
Good Practice: Comments should not be the only way that you document your code. Choosing sensible names for variables and functions, writing unit tests, and creating actual documents are other ways to document your code.
In English, we indicate the end of a sentence with a full stop. A sentence can be composed of multiple words and phrases, with the order of words being part of the grammar. For example, in English, we say “the black cat.”
The adjective, black, comes before the noun, cat. Whereas French grammar has a different order; the adjective comes after the noun: “le chat noir.” What’s important to take away from this is that the order matters.
C# indicates the end of a statement with a semicolon. A statement can be composed of multiple types, variables, and expressions made up of tokens. Each token is separated by white space or some other recognizably different token, like an operator, for example, =
or +
.
For example, in the following statement, decimal
is a type, totalPrice
is a variable, and subtotal + salesTax
is an expression:
decimal totalPrice = subtotal + salesTax;
The expression is made up of an operand named subtotal
, an operator +
, and another operand named salesTax
. The order of operands and operators matters because the order affects the meaning and result.
Comments are the primary method of documenting your code to increase an understanding of how it works, for other developers to read, or for you to read even when you come back to it months later.
In Chapter 4, Writing, Debugging, and Testing Functions, you will learn about XML comments that start with three slashes, ///
, and work with a tool to generate web pages to document your code.
You can add comments to explain your code using a double slash, //
. The compiler will ignore everything after the //
until the end of the line, as shown in the following code:
// Sales tax must be added to the subtotal.
var totalPrice = subtotal + salesTax;
To write a multiline comment, use /*
at the beginning and */
at the end of the comment, as shown in the following code:
/*
This is a
multi-line comment.
*/
Although /* */
is mostly used for multiline comments, it is also useful for commenting in the middle of a statement, as shown in the following code:
decimal totalPrice = subtotal /* for this item */ + salesTax;
Good Practice: Well-designed code, including function signatures with well-named parameters and class encapsulation, can be somewhat self-documenting. When you find yourself putting too many comments and explanations in your code, ask yourself: can I rewrite, aka refactor, this code to make it more understandable without long comments?
Your code editor has commands to make it easier to add and remove comment characters, as shown in the following list:
Good Practice: You comment code by adding descriptive text above or after code statements. You comment out code by adding comment characters before or around statements to make them inactive. Uncommenting means removing the comment characters.
In English, we indicate a new paragraph by starting a new line. C# indicates a block of code with the use of curly brackets, { }
.
Blocks start with a declaration to indicate what is being defined. For example, a block can define the start and end of many language constructs, including namespaces, classes, methods, or statements like foreach
.
You will learn more about namespaces, classes, and methods later in this chapter and subsequent chapters, but to briefly introduce some of those concepts now:
Code editors like Visual Studio 2022 and Visual Studio Code provide a handy feature to collapse and expand blocks by toggling the [-]
or [+]
or an arrow symbol pointing down or right when you move your mouse cursor over the left margin of the code, as shown in Figure 2.2:
Figure 2.2: Code editors with expanded and collapsed blocks
You can define your own labeled regions around any statements you want and then most code editors will allow you to collapse and expand them in the same way as blocks, as shown in the following code:
#region Three variables that store the number 2 million.
int decimalNotation = 2_000_000;
int binaryNotation = 0b_0001_1110_1000_0100_1000_0000;
int hexadecimalNotation = 0x_001E_8480;
#endregion
In this way, regions can be treated as commented blocks that can be collapsed to show a summary of what the block does.
I will use #region
blocks throughout the solution code in the GitHub repository, especially for the early chapters before we start defining functions that act as natural collapsible regions, but I won’t show them in the print book, to save space. Use your own judgment to decide if you want to use regions in your own code.
In a simple console app that does not use the top-level program feature, I’ve added some comments to the statements and blocks, as shown in the following code:
using System; // A semicolon indicates the end of a statement.
namespace Basics
{ // An open brace indicates the start of a block.
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!"); // A statement.
}
}
} // A close brace indicates the end of a block.
Note that C# uses a brace style where both the open and close braces are on their own line and are at the same indentation level, as shown in the following code:
if (x < 3)
{
// Do something if x is less than 3.
}
Other languages like JavaScript use curly braces but format them differently. They put the open curly brace at the end of the declaration statement, as shown in the following code:
if (x < 3) {
// Do something if x is less than 3.
}
You can use whatever style you prefer because the compiler does not care.
Sometimes, to save vertical space in a print book, I use the JavaScript brace style, but mostly I stick with the C# brace style. I use two spaces instead of the more common four spaces for indenting because my code will be printed in a book and therefore has narrow width available.
More Information: The official coding style conventions can be found at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions.
Regardless of any official guidelines, I recommend that you conform to whatever standards have been adopted by your development team unless you are a solo developer, in which case as long as your code compiles, you can use any conventions you like. Be kind to your future self though by being consistent one way or the other!
Good Practice: The brace style used in the Microsoft official documentation is the most commonly used for C#. For example, see the for
statement, as found at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/statements/iteration-statements.
White space includes the space, tab, and newline characters. You can use white space to format your code however you like because extra white space has no effect on the compiler.
The following four statements are all equivalent:
int sum = 1 + 2; // Most developers would prefer this format.
int
sum=1+
2; // One statement over three lines.
int sum= 1 +2;int sum=1+2; // Two statements on one line.
The only white space character required in the preceding statements is one between int
and sum
to tell the compiler they are separate tokens. Any single white space character, for example a space, tab, or newline would be acceptable.
More Information: You can read the formal definition of C# white space at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure#634-white-space.
The C# vocabulary is made up of keywords, symbol characters, and types.
Some of the predefined, reserved keywords that you will see in this book and use frequently include using
, namespace
, class
, static
, int
, string
, double
, bool
, if
, switch
, break
, while
, do
, for
, foreach
, this
, and true
.
Some of the symbol characters that you will see include "
, '
, +
, -
, *
, /
, %
, @
, and $
.
There are other contextual keywords that only have a special meaning in a specific context, like and
, or
, not
, record
, and init
.
However, that still means that there are only about 100 actual C# keywords in the language.
Good Practice: C# keywords use all lowercase. Although you can use all lowercase for your own type names, you should not. With C# 11 and later, the compiler will give a warning if you do, as shown in the following output: Warning CS8981 The type name 'person' only contains lower-cased ascii characters. Such names may become reserved for the language.
The English language has more than 250,000 distinct words, so how does C# get away with only having about 100 keywords? Moreover, why is C# so difficult to learn if it has only 0.0416% of the number of words in the English language?
One of the key differences between a human language and a programming language is that developers need to be able to define the new “words” with new meanings. Apart from the (about) 100 keywords in the C# language, this book will teach you about some of the hundreds of thousands of “words” that other developers have defined, but you will also learn how to define your own “words.”
Programmers all over the world must learn English because most programming languages use English words such as “if” and “break.” There are programming languages that use other human languages, such as Arabic, but they are rare. If you are interested in learning more, this YouTube video shows a demonstration of an Arabic programming language: https://youtu.be/dkO8cdwf6v8.
By default, Visual Studio 2022 and Visual Studio Code show C# keywords in blue to make them easier to differentiate from other code, which defaults to black. Both tools allow you to customize the color scheme.
In Visual Studio 2022:
In Visual Studio Code:
In JetBrains Rider:
Plain text editors such as Notepad don’t help you write correct English. Likewise, Notepad won’t help you write the correct C# either.
Microsoft Word can help you write English by highlighting spelling mistakes with red squiggles, with Word saying that “icecream” should be ice-cream or ice cream, and grammatical errors with blue squiggles, such as a sentence should have an uppercase first letter.
Similarly, Visual Studio 2022 and Visual Studio Code’s C# extension help you write C# code by highlighting spelling mistakes, such as the method name needing to be WriteLine
with an uppercase L
, and grammatical errors, such as statements that must end with a semicolon.
The C# extension constantly watches what you type and gives you feedback by highlighting problems with colored squiggly lines, like that of Microsoft Word.
Let’s see it in action:
Program.cs
, change the L
in the WriteLine
method to lowercase.Figure 2.3: The Error List window showing two compile errors
System
is a namespace, which is like an address for a type. To refer to someone’s location exactly, you might use Oxford.HighStreet.BobSmith
, which tells us to look for a person named Bob Smith on the High Street in the city of Oxford.
System.Console.WriteLine
tells the compiler to look for a method named WriteLine
in a type named Console
in a namespace named System
.
To simplify our code, the Console App project template for every version of .NET before 6.0 added a statement at the top of the code file to tell the compiler to always look in the System
namespace for types that haven’t been prefixed with their namespace, as shown in the following code:
using System; // Import the System namespace.
We call this importing the namespace. The effect of importing a namespace is that all available types in that namespace will be available to your program without needing to enter the namespace prefix. All available types in that namespace will be seen in IntelliSense while you write code.
Traditionally, every .cs
file that needs to import namespaces would have to start with using
statements to import those namespaces. Namespaces like System
and System.Linq
are needed in almost all .cs
files, so the first few lines of every .cs
file often had at least a few using
statements, as shown in the following code:
using System;
using System.Linq;
using System.Collections.Generic;
When creating websites and services using ASP.NET Core, there are often dozens of namespaces that each file would have to import.
C# 10 introduced a new keyword combination and .NET SDK 6 introduced a new project setting that works together to simplify importing common namespaces.
The global using
keyword combination means you only need to import a namespace in one .cs
file and it will be available throughout all .cs
files instead of having to import the namespace at the top of every file that needs it. You could put global using
statements in the Program.cs
file, but I recommend creating a separate file for those statements named something like GlobalUsings.cs
with the contents being all your global using
statements, as shown in the following code:
global using System;
global using System.Linq;
global using System.Collections.Generic;
Good Practice: As developers get used to this new C# feature, I expect one naming convention for this file to become the de facto standard. As you are about to see, the related .NET SDK feature uses a similar naming convention.
Any projects that target .NET 6 or later, and therefore use the C# 10 or later compiler, generate a <ProjectName>.GlobalUsings.g.cs
file in the obj\Debug\net8.0
folder to implicitly globally import some common namespaces like System
. The specific list of implicitly imported namespaces depends on which SDK you target, as shown in Table 2.6:
SDK |
Implicitly imported namespaces |
|
|
|
Same as
|
|
Same as
|
Table 2.6: .NET SDKs and their implicitly imported namespaces
Let’s see the current autogenerated implicit imports file:
bin
and obj
folders are now visible.Vocabulary
project, expand the obj
folder, expand the Debug
folder, expand the net8.0
folder, and then open the file named Vocabulary.GlobalUsings.g.cs
. The naming convention for this file is <ProjectName>.GlobalUsings.g.cs
. Note the g for generated to differentiate it from developer-written code files.
System.Threading
, as shown in the following code:
// <autogenerated />
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Threading;
global using global::System.Threading.Tasks;
Vocabulary.GlobalUsings.g.cs
file.Vocabulary.csproj
project file, and then add additional entries to the project file to control which namespaces are implicitly imported, as shown highlighted in the following markup:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<Using Remove="System.Threading" />
<Using Include="System.Numerics" />
<Using Include="System.Console" Static="true" />
<Using Include="System.Environment" Alias="Env" />
</ItemGroup>
</Project>
Note that <ItemGroup>
is different from <ImportGroup>
. Be sure to use the correct one! Also, note that the order of elements in a project group or item group does not matter. For example, <Nullable>
can be before or after <ImplicitUsings>
.
obj
folder, expand the Debug
folder, expand the net8.0
folder, and open the file named Vocabulary.GlobalUsings.g.cs
.System.Numerics
instead of System.Threading
, the Environment
class has been imported and aliased to Env
, and we have statically imported the Console
class, as shown highlighted in the following code:
// <autogenerated />
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Numerics;
global using global::System.Threading.Tasks;
global using Env = global::System.Environment;
global using static global::System.Console;
Program.cs
, add a statement to output a message from the computer and note that because we statically imported the Console
class, we can call its methods like WriteLine
without prefixing them with Console
, and we can reference the Environment
class using its alias Env
, as shown in the following code:
WriteLine($"Computer named {Env.MachineName} says \"No.\"");
Computer named DAVROS says "No."
Your computer name will be different unless you name your computers after characters from Doctor Who like I do.
You can disable the implicitly imported namespaces feature for all SDKs by removing the <ImplicitUsings>
element completely from the project file, or changing its value to disable
, as shown in the following markup:
<ImplicitUsings>disable</ImplicitUsings>
Good Practice: You might choose to do this if you want to manually create a single file with all the global using
statements instead of potentially having one generated automatically and others created manually. But my recommendation is to leave the feature enabled and modify the project file to change what is included in the auto-generated class file in the obj
folder hierarchy.
In English, verbs are doing or action words, like “run” and “jump.” In C#, doing or action words are called methods. There are hundreds of thousands of methods available to C#. In English, verbs change how they are written based on when in time the action happens. For example, Amir was jumping in the past, Beth jumps in the present, they jumped in the past, and Charlie will jump in the future.
In C#, methods such as WriteLine
change how they are called or executed based on the specifics of the action. This is called overloading, which we’ll cover in more detail in Chapter 5, Building Your Own Types with Object-Oriented Programming. But for now, consider the following example:
// Outputs the current line terminator.
// By default, this is a carriage-return and line feed.
Console.WriteLine();
// Outputs the greeting and the current line terminator.
Console.WriteLine("Hello Ahmed");
// Outputs a formatted number and date and the current line terminator.
Console.WriteLine(
"Temperature on {0:D} is {1}°C.", DateTime.Today, 23.4);
When I show code snippets without numbered step-by-step instructions, I do not expect you to enter them as code, so they won’t execute out of context.
A different and not quite exact analogy is that some verbs are spelled the same but have different effects depending on the context, for example, you can lose a game, lose your place in a book, or lose your keys.
In English, nouns are names that refer to things. For example, Fido is the name of a dog. The word “dog” tells us the type of thing that Fido is, and so to order Fido to fetch a ball, we would use his name.
In C#, their equivalents are types, variables, fields, and properties. For example:
Animal
and Car
are types; they are nouns for categorizing things.Head
and Engine
might be fields or properties; they are nouns that belong to Animal
and Car
.Fido
and Bob
are variables; they are nouns for referring to a specific object.There are tens of thousands of types available to C#, though have you noticed how I didn’t say, “There are tens of thousands of types in C#”? The difference is subtle but important. The language of C# only has a few keywords for types, such as string
and int
, and strictly speaking, C# doesn’t define any types. Keywords such as string
that look like types are aliases, which represent types provided by the platform on which C# runs.
It’s important to know that C# cannot exist alone; after all, it’s a language that runs on variants of .NET. In theory, someone could write a compiler for C# that uses a different platform, with different underlying types. In practice, the platform for C# is .NET, which provides tens of thousands of types to C#, including System.Int32
, which is the C# keyword alias int
maps to, as well as many more complex types, such as System.Xml.Linq.XDocument
.
It’s worth taking note that the term type is often confused with class. Have you ever played the parlor game Twenty Questions, also known as Animal, Vegetable, or Mineral? In the game, everything can be categorized as an animal, vegetable, or mineral. In C#, every type can be categorized as a class
, struct
, enum
, interface
, or delegate
. You will learn what these mean in Chapter 6, Implementing Interfaces and Inheriting Classes. As an example, the C# keyword string
is a class
, but int
is a struct
. So, it is best to use the term type to refer to both.
We know that there are more than 100 keywords in C#, but how many types are there? Let’s write some code to find out how many types (and their methods) are available to C# in our simple console app.
Don’t worry about exactly how this code works for now, but know that it uses a technique called reflection:
Program.cs
.System.Reflection
namespace at the top of the Program.cs
file so that we can use some of the types in that namespace like Assembly
and TypeName
, as shown in the following code:
using System.Reflection; // To use Assembly, TypeName, and so on.
Good Practice: We could use the implicit imports and global using
features to import this namespace for all .cs
files in this project, but since there is only one file, it is better to import the namespace in the one file in which it is needed.
// Get the assembly that is the entry point for this app.
Assembly? myApp = Assembly.GetEntryAssembly();
// If the previous line returned nothing then end the app.
if (myApp is null) return;
// Loop through the assemblies that my app references.
foreach (AssemblyName name in myApp.GetReferencedAssemblies())
{
// Load the assembly so we can read its details.
Assembly a = Assembly.Load(name);
// Declare a variable to count the number of methods.
int methodCount = 0;
// Loop through all the types in the assembly.
foreach (TypeInfo t in a.DefinedTypes)
{
// Add up the counts of all the methods.
methodCount += t.GetMethods().Length;
}
// Output the count of types and their methods.
WriteLine("{0:N0} types with {1:N0} methods in {2} assembly.",
arg0: a.DefinedTypes.Count(),
arg1: methodCount,
arg2: name.Name);
}
N0
is uppercase N
followed by the digit zero. It is not uppercase N
followed by uppercase O
. It means “format a number (N
) with zero (0
) decimal places.”
0 types with 0 methods in System.Runtime assembly.
41 types with 639 methods in System.Console assembly.
112 types with 1,190 methods in System.Linq assembly.
Why does the System.Runtime
assembly contain zero types? This assembly is special because it contains only type-forwarders rather than actual types. A type-forwarder represents a type that has been implemented outside of .NET or for some other advanced reason.
System.Reflection
namespace) to declare some variables, as shown in the following code:
// Declare some unused variables using types in
// additional assemblies to make them load too.
System.Data.DataSet ds = new();
HttpClient client = new();
0 types with 0 methods in System.Runtime assembly.
406 types with 7,139 methods in System.Data.Common assembly.
425 types with 4,681 methods in System.Net.Http assembly.
41 types with 639 methods in System.Console assembly.
112 types with 1,190 methods in System.Linq assembly.
Now, you have a better sense of why learning C# is a challenge – because there are so many types and methods to learn. Methods are only one category of a member that a type can have, and you and other programmers are constantly defining new types and members!
The code we wrote in the Vocabulary
project is a good example of when you might want to ask ChatGPT to explain something. If you enter a prompt of “Explain the following code:” followed by the code, here’s what it responded with at the time of writing in July 2023:
“This C# code performs an analysis of the assemblies (essentially libraries or modules) referenced by the current application. It tallies up the total number of types and methods in each referenced assembly.
Let’s break down the code in more detail:
System.Reflection
namespace is used to work with assemblies
, modules
, types
, methods
, and so on, at runtime.System.Data.DataSet
and HttpClient
instances are declared but not used. The comment implies that these are declared to ensure their respective assemblies are loaded for the subsequent analysis. Creating instances of classes from certain assemblies can cause those assemblies to be loaded into memory.Assembly.GetEntryAssembly()
gets the assembly that is the entry point of the application.myApp
is null (meaning there’s no entry assembly, which should be impossible in a normally running .NET app), the application exits.foreach
loop iterates over each assembly name in the referenced assemblies of the entry point assembly.Assembly.Load(name)
loads an assembly given the assembly
name. This allows you to interact with the assembly programmatically.methodCount
is declared and initialized to 0
. This is used to count the total number of methods in the assembly.foreach
loop then iterates over each type in the assembly.(t.GetMethods().Length)
and adds it to the methodCount
.WriteLine()
. The {0:N0}
format specifier in the string is a placeholder for a number (0
refers to the argument index, N
is for number format, and 0 after N specifies the number of decimal places).So, in essence, this script counts how many types and methods exist in each assembly referenced by the application. This might be useful to understand the complexity or size of the dependencies the application has.”
And now back to the flesh and blood Mark: That’s a pretty good explanation with no mistakes or hallucinations!
Good Practice: Tools like GitHub Copilot and ChatGPT are especially useful for programmers when they are learning new things. And a good programmer is always learning new things! These tools are not just for writing code for you. They can explain existing code written by others and even suggest improvements.
All applications process data. Data comes in, data is processed, and then data goes out.
Data usually comes into our program from files, databases, or user input, and it can be put temporarily into variables, which will be stored in the memory of the running program. When the program ends, the data in memory is lost. Data is usually output to files and databases, or to the screen or a printer. When using variables, you should think about, firstly, how much space the variable takes up in the memory, and, secondly, how fast it can be processed.
We control this by picking an appropriate type. You can think of simple common types such as int
and double
as being different-sized storage boxes, where a smaller box would take less memory but may not be as fast at being processed; for example, adding 16-bit numbers might not be processed as quickly as adding 64-bit numbers on a 64-bit operating system. Some of these boxes may be stacked close by, and some may be thrown into a big heap further away.
There are naming conventions for things, and it is a good practice to follow them, as shown in Table 2.7:
Naming convention |
Examples |
Used for |
Camel case |
|
Local variables, private fields. |
Title case aka Pascal case |
|
Types, non-private fields, and other members like methods. |
Table 2.7: Naming conventions and what they should be used for
Some C# programmers like to prefix the names of private fields with an underscore, for example, _dateOfBirth
instead of dateOfBirth
. The naming of private members of all kinds is not formally defined because they will not be visible outside the class, so writing them with or without an underscore prefix are both valid.
Good Practice: Following a consistent set of naming conventions will enable your code to be easily understood by other developers (and yourself in the future!).
The following code block shows an example of declaring a named local variable and assigning a value to it with the =
symbol. You should note that you can output the name of a variable using a keyword introduced in C# 6, nameof
:
// Let the heightInMetres variable become equal to the value 1.88.
double heightInMetres = 1.88;
Console.WriteLine($"The variable {nameof(heightInMetres)} has the value
{heightInMetres}.");
Warning! The message in double quotes in the preceding code wraps onto a second line because the width of a printed page is too narrow. When entering a statement like this in your code editor, type it all in a single line.
In C# 12, nameof
can now access instance data from a static context. You will learn the difference between instance and static data in Chapter 5, Building Your Own Types with Object-Oriented Programming.
When you assign to a variable, you often, but not always, assign a literal value. But what is a literal value? A literal is a notation that represents a fixed value. Data types have different notations for their literal values, and over the next few sections, you will see examples of using literal notation to assign values to variables.
More Information: You can read the formal definition of literals in the C# language specification: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure#645-literals.
For text, a single letter, such as an A
, is stored as a char
type.
Good Practice: Actually, it can be more complicated than that. Egyptian Hieroglyph A002 (U+13001) needs two System.Char
values (known as surrogate pairs) to represent it: \uD80C
and \uDC01
. Do not always assume one char
equals one letter or you could introduce hard-to-notice bugs into your code.
A char
is assigned using single quotes around the literal value, or assigning the return value of a function call, as shown in the following code:
char letter = 'A'; // Assigning literal characters.
char digit = '1';
char symbol = '$';
char userChoice = GetChar(); // Assigning from a fictitious function.
For text, multiple letters, such as Bob
, are stored as a string
type and are assigned using double quotes around the literal value, or by assigning the return value of a function call or constructor, as shown in the following code:
string firstName = "Bob"; // Assigning literal strings.
string lastName = "Smith";
string phoneNumber = "(215) 555-4256";
// Assigning a string returned from the string class constructor.
string horizontalLine = new('-', count: 74); // 74 hyphens.
// Assigning a string returned from a fictitious function.
string address = GetAddressFromDatabase(id: 563);
// Assigning an emoji by converting from Unicode.
string grinningEmoji = char.ConvertFromUtf32(0x1F600);
To output emojis at a command prompt on Windows, you must use Windows Terminal because Command Prompt does not support emojis, and set the output encoding of the console to use UTF-8, as shown in the following code:
Console.OutputEncoding = System.Text.Encoding.UTF8;
string grinningEmoji = char.ConvertFromUtf32(0x1F600);
Console.WriteLine(grinningEmoji);
When storing text in a string
variable, you can include escape sequences, which represent special characters like tabs and new lines using a backslash, as shown in the following code:
string fullNameWithTabSeparator = "Bob\tSmith";
But what if you are storing the path to a file on Windows, and one of the folder names starts with a T
, as shown in the following code?
string filePath = "C:\televisions\sony\bravia.txt";
The compiler will convert the \t
into a tab character and you will get errors!
You must prefix it with the @
symbol to use a verbatim literal string
, as shown in the following code:
string filePath = @"C:\televisions\sony\bravia.txt";
Introduced in C# 11, raw string literals are convenient for entering any arbitrary text without needing to escape the contents. They make it easy to define literals containing other languages like XML, HTML, or JSON.
Raw string literals start and end with three or more double-quote characters, as shown in the following code:
string xml = """
<person age="50">
<first_name>Mark</first_name>
</person>
""";
Why three or more double-quote characters? This is for scenarios where the content itself needs to have three double-quote characters; you can then use four double-quote characters to indicate the beginning and end of the content. Where the content needs to have four double-quote characters, you can then use five double-quote characters to indicate the beginning and end of the content. And so on.
In the previous code, the XML is indented by 13 spaces. The compiler looks at the indentation of the last three or more double-quote characters, and then automatically removes that level of indentation from all the content inside the raw string literal. The results of the previous code would therefore not be indented as in the defining code, but instead be aligned with the left margin, as shown in the following markup:
<person age="50">
<first_name>Mark</first_name>
</person>
If the end three double-quote characters are aligned with the left margin, as shown in the following code:
string xml = """
<person age="50">
<first_name>Mark</first_name>
</person>
""";
Then the 13-space indentation would not be removed, as shown in the following markup:
<person age="50">
<first_name>Mark</first_name>
</person>
You can mix interpolated strings that use curly braces {
}
with raw string literals. You specify the number of braces that indicates a replaced expression by adding that number of dollar signs to the start of the literal. Any fewer braces than that are treated as raw content.
For example, if we want to define some JSON, single braces will be treated as normal braces, but the two dollar symbols tell the compiler that any two curly braces indicate a replaced expression value, as shown in the following code:
var person = new { FirstName = "Alice", Age = 56 };
string json = $$"""
{
"first_name": "{{person.FirstName}}",
"age": {{person.Age}},
"calculation": "{{{ 1 + 2 }}}"
}
""";
Console.WriteLine(json);
The previous code would generate the following JSON document:
{
"first_name": "Alice",
"age": 56,
"calculation": "{3}"
}
The number of dollars tells the compiler how many curly braces are needed to become recognized as an interpolated expression.
To summarize:
\t
for tab. To represent a backslash, use two: \\
.@
to disable escape characters so that a backslash is a backslash. It also allows the string
value to span multiple lines because the whitespace characters are treated as themselves instead of instructions to the compiler.$
to enable embedded formatted variables. You will learn more about this later in this chapter.Numbers are data that we want to perform an arithmetic calculation on, for example, multiplying. A telephone number is not a number. To decide whether a variable should be stored as a number or not, ask yourself whether you need to perform arithmetic operations on the number or whether the number includes non-digit characters such as parentheses or hyphens to format the number, such as (414) 555-1234. In this case, the “number” is a sequence of characters, so it should be stored as a string
.
Numbers can be natural numbers, such as 42, used for counting (also called whole numbers); they can also be negative numbers, such as -42 (called integers); or they can be real numbers, such as 3.9 (with a fractional part), which are called single- or double-precision floating-point numbers in computing.
Let’s explore numbers:
console
project named Numbers
to the Chapter02
solution.Program.cs
, delete the existing code, and then type statements to declare some number variables using various data types, as shown in the following code:
// An unsigned integer is a positive whole number or 0.
uint naturalNumber = 23;
// An integer is a negative or positive whole number or 0.
int integerNumber = -23;
// A float is a single-precision floating-point number.
// The F or f suffix makes the value a float literal.
// The suffix is required to compile.
float realNumber = 2.3f;
// A double is a double-precision floating-point number.
// double is the default for a number value with a decimal point.
double anotherRealNumber = 2.3; // A double literal value.
You might know that computers store everything as bits. The value of a bit is either 0
or 1
. This is called a binary number system. Humans use a decimal number system.
The decimal number system, also known as Base 10, has 10 as its base, meaning there are 10 digits, from 0 to 9. Although it is the number base most used by human civilizations, other number base systems are popular in science, engineering, and computing. The binary number system, also known as Base 2, has two as its base, meaning there are two digits, 0 and 1.
The following image shows how computers store the decimal number 10. Take note of the bits with the value 1 in the 8 and 2 columns; 8 + 2 = 10:
Figure 2.4: How computers store the decimal number 10
So, 10
in decimal is 00001010
in a binary byte (8 bits).
Two of the improvements seen in C# 7 and later are the use of the underscore character _
as a digit separator and support for binary literals.
You can insert underscores anywhere into the digits of a number literal, including decimal, binary, or hexadecimal notation, to improve legibility.
For example, you could write the value for 1 million in decimal notation, that is, Base 10, as 1_000_000
.
You can even use the 2/3 grouping common in India: 10_00_000
.
To use binary notation, that is, Base 2, using only 1s and 0s, start the number literal with 0b
. To use hexadecimal notation, that is, Base 16, using 0 to 9 and A to F, start the number literal with 0x
.
Let’s enter some code to see some examples:
Numbers
project, in Program.cs
, type statements to declare some number variables using underscore separators, as shown in the following code:
int decimalNotation = 2_000_000;
int binaryNotation = 0b_0001_1110_1000_0100_1000_0000;
int hexadecimalNotation = 0x_001E_8480;
// Check the three variables have the same value.
Console.WriteLine($"{decimalNotation == binaryNotation}");
Console.WriteLine(
$"{decimalNotation == hexadecimalNotation}");
// Output the variable values in decimal.
Console.WriteLine($"{decimalNotation:N0}");
Console.WriteLine($"{binaryNotation:N0}");
Console.WriteLine($"{hexadecimalNotation:N0}");
// Output the variable values in hexadecimal.
Console.WriteLine($"{decimalNotation:X}");
Console.WriteLine($"{binaryNotation:X}");
Console.WriteLine($"{hexadecimalNotation:X}");
True
True
2,000,000
2,000,000
2,000,000
1E8480
1E8480
1E8480
Computers can always exactly represent integers using the int
type or one of its sibling types, such as long
and short
.
Computers cannot always represent real, aka decimal or non-integer, numbers precisely. The float
and double
types store real numbers using single- and double-precision floating points.
Most programming languages implement the Institute of Electrical and Electronics Engineers (IEEE) Standard for Floating-Point Arithmetic. IEEE 754 is a technical standard for floating-point arithmetic established in 1985 by the IEEE.
The following image shows a simplification of how a computer represents the number 12.75
in binary notation. Note the bits with the value 1
in the 8, 4, ½, and ¼ columns.
8 + 4 + ½ + ¼ = 12¾ = 12.75.
Figure 2.5: Computer representing the number 12.75 in binary notation
So, 12.75
in decimal is 00001100.1100
in binary. As you can see, the number 12.75
can be exactly represented using bits. However, most numbers can’t, which is something that we’ll be exploring shortly.
C# has an operator named sizeof()
that returns the number of bytes that a type uses in memory. Some types have members named MinValue
and MaxValue
, which return the minimum and maximum values that can be stored in a variable of that type. We are now going to use these features to create a console app to explore number types:
Program.cs
, at the bottom of the file, type statements to show the size of three number data types, as shown in the following code:
Console.WriteLine($"int uses {sizeof(int)} bytes and can store numbers in the range {int.MinValue:N0} to {int.MaxValue:N0}.");
Console.WriteLine($"double uses {sizeof(double)} bytes and can store numbers in the range {double.MinValue:N0} to {double.MaxValue:N0}.");
Console.WriteLine($"decimal uses {sizeof(decimal)} bytes and can store numbers in the range {decimal.MinValue:N0} to {decimal.MaxValue:N0}.");
Warning! The width of the printed pages in this book makes the string
values (in double quotes) wrap over multiple lines. You must type them on a single line, or you will get compile errors.
Figure 2.6: Size and range information for common number data types
An int
variable uses four bytes of memory and can store positive or negative numbers up to about 2 billion. A double
variable uses 8 bytes of memory and can store much bigger values! A decimal
variable uses 16 bytes of memory and can store big numbers, but not as big as a double
type.
But you may be asking yourself, why might a double
variable be able to store bigger numbers than a decimal
variable, yet it’s only using half the space in memory? Well, let’s now find out!
You will now write some code to compare double
and decimal
values. Although it isn’t hard to follow, don’t worry about understanding the syntax right now:
double
variables, add them together, and compare them to the expected result. Then, write the result to the console, as shown in the following code:
Console.WriteLine("Using doubles:");
double a = 0.1;
double b = 0.2;
if (a + b == 0.3)
{
Console.WriteLine($"{a} + {b} equals {0.3}");
}
else
{
Console.WriteLine($"{a} + {b} does NOT equal {0.3}");
}
Using doubles:
0.1 + 0.2 does NOT equal 0.3
In cultures that use a comma for the decimal separator, the result will look slightly different, as shown in the following output: 0,1 + 0,2 does NOT equal 0,3
.
The double
type is not guaranteed to be accurate because most numbers like 0.1
, 0.2
, and 0.3
literally cannot be exactly represented as floating-point values.
If you were to try different values, like 0.1 + 0.3 == 0.4
, it would happen to return true
because with double
values, some imprecise values happen to be exactly equal in their current representation even though they might not actually be equal mathematically. So, some numbers can be directly compared but some cannot. I deliberately picked 0.1
and 0.2
to compare to 0.3
because they cannot be compared, as proven by the result.
You could compare real numbers stored in the float
type, which is less accurate than the double
type, but the comparison would actually appear to be true
because of that lower accuracy!
float a = 0.1F;
float b = 0.2F;
if (a + b == 0.3F) // True because float is less "accurate" than double.
...
As a rule of thumb, you should only use double
when accuracy, especially when comparing the equality of two numbers, is not important. An example of this might be when you’re measuring a person’s height; you will only compare values using greater than or less than, but never equals.
The problem with the preceding code is illustrated by how the computer stores the number 0.1
, or multiples of it. To represent 0.1
in binary, the computer stores 1 in the 1/16 column, 1 in the 1/32 column, 1 in the 1/256 column, 1 in the 1/512 column, and so on.
The number 0.1
in decimal is 0.00011001100110011
… in binary, repeating forever:
Figure 2.7: Number 0.1 in decimal repeating forever in binary
Good Practice: Never compare double
values using ==
. During the First Gulf War, an American Patriot missile battery used double
values in its calculations. The inaccuracy caused it to fail to track and intercept an incoming Iraqi Scud missile, and 28 soldiers were killed; you can read about this at https://www.ima.umn.edu/~arnold/disasters/patriot.html. The Patriot missile system has improved since then. “Forty years after it was brought into service, the Patriot air-defense system is finally doing what it was designed for.” “No one was 100% sure that the Patriot was capable of destroying a Kh-47 hypersonic missile,” said Col. Serhiy Yaremenko, commander of the 96th Anti-Aircraft Missile Brigade, which defends Kyiv. “Ukrainians proved it.”: https://archive.ph/2023.06.11-132200/https://www.wsj.com/amp/articles/u-s-patriot-missile-is-an-unsung-hero-of-ukraine-war-db6053a0.
Now let’s see the same code using the decimal
number type:
double
variables).decimal
and rename the variables to c
and d
, as shown in the following code:
Console.WriteLine("Using decimals:");
decimal c = 0.1M; // M suffix means a decimal literal value
decimal d = 0.2M;
if (c + d == 0.3M)
{
Console.WriteLine($"{c} + {d} equals {0.3M}");
}
else
{
Console.WriteLine($"{c} + {d} does NOT equal {0.3M}");
}
Using decimals:
0.1 + 0.2 equals 0.3
The decimal
type is accurate because it stores the number as a large integer and shifts the decimal point. For example, 0.1
is stored as 1
, with a note to shift the decimal point one place to the left. 12.75
is stored as 1275
, with a note to shift the decimal point two places to the left.
Good Practice: Use int
for whole numbers. Use double
for real numbers that will not be compared for equality to other values; it is okay to compare double
values being less than or greater than, and so on. Use decimal
for money, CAD drawings, general engineering, and wherever the accuracy of a real number is important.
The float
and double
types have some useful special values: NaN
represents not-a-number (for example, the result of dividing by zero), Epsilon
represents the smallest positive number that can be stored in a float
or double
, and PositiveInfinity
and NegativeInfinity
represent infinitely large positive and negative values. They also have methods for checking for these special values like IsInfinity
and IsNaN
.
The System.Half
type was introduced in .NET 5. Like float
and double
, it can store real numbers. It normally uses two bytes of memory. The System.Int128
and System.UInt128
types were introduced in .NET 7. Like int
and uint
, they can store signed (positive and negative) and unsigned (only zero and positive) integer values. They normally use 16 bytes of memory.
For these new number types, the sizeof
operator only works in an unsafe code block, and you must compile the project using an option to enable unsafe code. Let’s explore how this works:
Program.cs
, at the bottom of the file, type statements to show the size of the Half
and Int128
number data types, as shown in the following code:
unsafe
{
Console.WriteLine($"Half uses {sizeof(Half)} bytes and can store numbers in the range {Half.MinValue:N0} to {Half.MaxValue:N0}.");
Console.WriteLine($"Int128 uses {sizeof(Int128)} bytes and can store numbers in the range {Int128.MinValue:N0} to {Int128.MaxValue:N0}.");
}
Numbers.csproj
, add an element to enable unsafe code, as shown highlighted in the following markup:
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
Numbers
project and note the sizes of the two new number types, as shown in the following output:
Half uses 2 bytes and can store numbers in the range -65,504 to 65,504.
Int128 uses 16 bytes and can store numbers in the range -170,141,183,460,469,231,731,687,303,715,884,105,728 to 170,141,183,460,469,231,731,687,303,715,884,105,727.
More Information: The sizeof
operator requires an unsafe code block except for the commonly used types like int
and byte
. You can learn more about sizeof
at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/sizeof. Unsafe code cannot have its safety verified. You can learn more about unsafe code blocks at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/unsafe-code.
Booleans can only contain one of the two literal values true
or false
, as shown in the following code:
bool happy = true;
bool sad = false;
They are most used to branch and loop. You don’t need to fully understand them yet, as they are covered more in Chapter 3, Controlling Flow, Converting Types, and Handling Exceptions.
There is a special type named object
that can store any type of data, but its flexibility comes at the cost of messier code and possibly poor performance. Because of those two reasons, you should avoid it whenever possible. The following steps show you how to use object types if you need to use them because you must use a Microsoft or third-party library that uses them:
console
project named Variables
to the Chapter02
solution.Program.cs
, delete the existing statements and then type statements to declare and use some variables using the object
type, as shown in the following code:
object height = 1.88; // Storing a double in an object.
object name = "Amir"; // Storing a string in an object.
Console.WriteLine($"{name} is {height} metres tall.");
int length1 = name.Length; // This gives a compile error!
int length2 = ((string)name).Length; // Cast name to a string.
Console.WriteLine($"{name} has {length2} characters.");
name
variable is not known by the compiler, as shown in Figure 2.8:Figure 2.8: The object type does not have a Length property
length
of a string
if the programmer explicitly tells the compiler that the object
variable contains a string
by prefixing the name
variable with a cast expression like (string)name
. The results can then successfully be written to the console, as shown in the following output:
Amir is 1.88 meters tall.
Amir has 4 characters.
You will learn about cast expressions in Chapter 3, Controlling Flow, Converting Types, and Handling Exceptions.
The object
type has been available since the first version of C#, but C# 2 and later have a better alternative called generics, which we will cover in Chapter 6, Implementing Interfaces and Inheriting Classes. This will provide us with the flexibility we want but without the performance overhead.
There is another special type named dynamic
that can also store any type of data, but even more than object
, its flexibility comes at the cost of performance. The dynamic
keyword was introduced in C# 4. However, unlike object
, the value stored in the variable can have its members invoked without an explicit cast. Let’s make use of a dynamic
type:
dynamic
variable. Assign a string
literal value, and then an integer value, and then an array of integer values. Finally, add a statement to output the length of the dynamic
variable, as shown in the following code:
dynamic something;
// Storing an array of int values in a dynamic object.
// An array of any type has a Length property.
something = new[] { 3, 5, 7 };
// Storing an int in a dynamic object.
// int does not have a Length property.
something = 12;
// Storing a string in a dynamic object.
// string has a Length property.
something = "Ahmed";
// This compiles but might throw an exception at run-time.
Console.WriteLine($"The length of something is {something.Length}");
// Output the type of the something variable.
Console.WriteLine($"something is a {something.GetType()}");
You will learn about arrays in Chapter 3, Controlling Flow, Converting Types, and Handling Exceptions.
string
value which does have a Length
property, as shown in the following output:
The length of something is 5
something is a System.String
string
value to the something
variable by prefixing the statement with two slashes //
.something
is an int
that does not have a Length
property, as shown in the following output:
Unhandled exception. Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'int' does not contain a definition for 'Length'
int
to the something
variable.int
values does have a Length
property, as shown in the following output:
The length of something is 3
something is a System.Int32[]
One limitation of dynamic
is that code editors cannot show IntelliSense to help you write the code. This is because the compiler cannot check what the type is during build time. Instead, the CLR checks for the member at runtime and throws an exception if it is missing.
Exceptions are a way to indicate that something has gone wrong at runtime. You will learn more about them and how to handle them in Chapter 3, Controlling Flow, Converting Types, and Handling Exceptions.
Dynamic types are most useful when interoperating with non-.NET systems. For example, you might need to work with a class library written in F#, Python, or some JavaScript. You might also need to interop with technologies like the Component Object Model (COM), for example, when automating Excel or Word.
Local variables are declared inside methods, and they only exist during the execution of that method. Once the method returns, the memory allocated to any local variables is released.
Strictly speaking, value types are released while reference types must wait for garbage collection. You will learn about the difference between value types and reference types and how to make sure that only one garbage collection is needed rather than two when releasing unmanaged resources in Chapter 6, Implementing Interfaces and Inheriting Classes.
Let’s explore local variables declared with specific types and using type inference:
int population = 67_000_000; // 67 million in UK.
double weight = 1.88; // in kilograms.
decimal price = 4.99M; // in pounds sterling.
string fruit = "Apples"; // string values use double-quotes.
char letter = 'Z'; // char values use single-quotes.
bool happy = true; // Booleans can only be true or false.
Depending on your code editor and color scheme, it will show green squiggles under each of the variable names and lighten their text color to warn you that the variable is assigned but its value is never used.
You can use the var
keyword to declare local variables with C# 3 and later. The compiler will infer the type from the value that you assign after the assignment operator, =
. This happens at compile time so using var
has no effect on runtime performance.
A literal number without a decimal point is inferred as an int
variable, that is, unless you add a suffix, as described in the following list:
L
: Compiler infers long
UL
: Compiler infers ulong
M
: Compiler infers decimal
D
: Compiler infers double
F
: Compiler infers float
A literal number with a decimal point is inferred as double
unless you add the M
suffix, in which case the compiler infers a decimal
variable, or the F
suffix, in which case it infers a float
variable.
Double quotes indicate a string
variable, single quotes indicate a char
variable, and the true
and false
values infer a bool
type:
var
, as shown in the following code:
var population = 67_000_000; // 67 million in UK.
var weight = 1.88; // in kilograms.
var price = 4.99M; // in pounds sterling.
var fruit = "Apples"; // string values use double-quotes.
var letter = 'Z'; // char values use single-quotes.
var happy = true; // Booleans can only be true or false.
var
keywords and note that your code editor shows a tooltip with information about the type that has been inferred.Program.cs
, import the namespace for working with XML to enable us to declare some variables using types in that namespace, as shown in the following code:
using System.Xml; // To use XmlDocument.
Program.cs
, add statements to create some new objects, as shown in the following code:
// Good use of var because it avoids the repeated type
// as shown in the more verbose second statement.
var xml1 = new XmlDocument(); // Works with C# 3 and later.
XmlDocument xml2 = new XmlDocument(); // Works with all C# versions.
// Bad use of var because we cannot tell the type, so we
// should use a specific type declaration as shown in
// the second statement.
var file1 = File.CreateText("something1.txt");
StreamWriter file2 = File.CreateText("something2.txt");
Good Practice: Although using var
is convenient, some developers avoid using it to make it easier for a code reader to understand the types in use. Personally, I use it only when the type is obvious. For example, in the preceding code statements, the first statement is just as clear as the second in stating what the types of the xml
variables are, but it is shorter. However, the third statement isn’t clear in showing the type of the file
variable, so the fourth is better because it shows that the type is StreamWriter
. If in doubt, spell it out!
With C# 9, Microsoft introduced another syntax for instantiating objects, known as target-typed new. When instantiating an object, you can specify the type first and then use new
without repeating the type, as shown in the following code:
XmlDocument xml3 = new(); // Target-typed new in C# 9 or later.
If you have a type with a field or property that needs to be set, then the type can be inferred, as shown in the following code:
// In Program.cs.
Person kim = new();
kim.BirthDate = new(1967, 12, 26); // i.e. new DateTime(1967, 12, 26)
// In a separate Person.cs file or at the bottom of Program.cs.
class Person
{
public DateTime BirthDate;
}
This way of instantiating objects is especially useful with arrays and collections because they have multiple objects, often of the same type, as shown in the following code:
List<Person> people = new() // Instead of: new List<Person>()
{
new() { FirstName = "Alice" }, // Instead of: new Person() { ... }
new() { FirstName = "Bob" },
new() { FirstName = "Charlie" }
};
You will learn about arrays in Chapter 3, Controlling Flow, Converting Types, and Handling Exceptions, and collections in Chapter 8, Working with Common .NET Types.
Good Practice: Use target-typed new to instantiate objects because it requires fewer characters, when reading a statement from left to right, as in English, you immediately know the type of the variable, and it is not limited to local variables like var
is. IMHO, the only reason not to use target-typed new is if you must use a pre-version 9 C# compiler. I do acknowledge that my opinion is not accepted by the whole C# community. I have used target-typed new throughout the remainder of this book. Please let me know if you spot any cases that I missed!
Most of the primitive types except string
are value types, which means that they must have a value. You can determine the default value of a type by using the default()
operator and passing the type as a parameter. You can assign the default value of a type by using the default
keyword.
The string
type is a reference type. This means that string
variables contain the memory address of a value, not the value itself. A reference type variable can have a null
value, which is a literal that indicates that the variable does not reference anything (yet). null
is the default for all reference types.
You’ll learn more about value types and reference types in Chapter 6, Implementing Interfaces and Inheriting Classes.
int
, a bool
, a DateTime
, and a string
, as shown in the following code:
Console.WriteLine($"default(int) = {default(int)}");
Console.WriteLine($"default(bool) = {default(bool)}");
Console.WriteLine($"default(DateTime) = {default(DateTime)}");
Console.WriteLine($"default(string) = {default(string)}");
null
values output as an empty string
, as shown in the following output:
default(int) = 0
default(bool) = False
default(DateTime) = 01/01/0001 00:00:00
default(string) =
int number = 13;
Console.WriteLine($"number set to: {number}");
number = default;
Console.WriteLine($"number reset to its default: {number}");
number set to: 13
number reset to its default: 0
We have already created and used basic console apps, but we’re now at a stage where we should delve into them more deeply.
Console apps are text-based and are run at the command prompt. They typically perform simple tasks that need to be scripted, such as compiling a file or encrypting a section of a configuration file.
Equally, they can also have arguments passed to them to control their behavior.
An example of this would be to create a new console app using the F# language with a specified name instead of using the name of the current folder, as shown in the following command:
dotnet new console -lang "F#" --name "ExploringConsole"
The two most common tasks that a console app performs are writing and reading data. We have already used the WriteLine
method to output, but if we didn’t want a carriage return at the end of a line, for example, if we later wanted to continue to write more text at the end of that line, we could have used the Write
method.
If you want to write three letters to the console without carriage returns after them, then call the Write
method, as shown in the following code:
Write("A");
Write("B");
Write("C");
This would write the three characters on a single line and leave the cursor at the end of the line, as shown in the following output:
ABC
If you want to write three letters to the console with carriage returns after them, then call the WriteLine
method, as shown in the following code:
WriteLine("A");
WriteLine("B");
WriteLine("C");
This would write three lines and leave the cursor on the fourth line:
A
B
C
One way of generating formatted strings is to use numbered positional arguments.
This feature is supported by methods like Write
and WriteLine
. For methods that do not support the feature, the string
parameter can be formatted using the Format
method of string
.
Let’s begin formatting:
console
project named Formatting
to the Chapter02
solution.Program.cs
, delete the existing statements and then type statements to declare some number variables and write them to the console, as shown in the following code:
int numberOfApples = 12;
decimal pricePerApple = 0.35M;
Console.WriteLine(
format: "{0} apples cost {1:C}",
arg0: numberOfApples,
arg1: pricePerApple * numberOfApples);
string formatted = string.Format(
format: "{0} apples cost {1:C}",
arg0: numberOfApples,
arg1: pricePerApple * numberOfApples);
//WriteToFile(formatted); // Writes the string into a file.
The WriteToFile
method is a nonexistent method used to illustrate the idea.
The Write
, WriteLine
, and Format
methods can have up to three numbered arguments, named arg0
, arg1
, and arg2
. If you need to pass more than three values, then you cannot name them.
Program.cs
, type statements to write three and then five arguments to the console, as shown in the following code:
// Three parameter values can use named arguments.
Console.WriteLine("{0} {1} lived in {2}.",
arg0: "Roger", arg1: "Cevung", arg2: "Stockholm");
// Four or more parameter values cannot use named arguments.
Console.WriteLine(
"{0} {1} lived in {2} and worked in the {3} team at {4}.",
"Roger", "Cevung", "Stockholm", "Education", "Optimizely");
Good Practice: Once you become more comfortable with formatting strings, you should stop naming the parameters, for example, stop using format:
, arg0:
, and arg1:
. The preceding code uses a non-canonical style to show where the 0
and 1
came from while you are learning.
If you use JetBrains Rider and you have installed the Unity Support plugin, then it will complain a lot about boxing. A common scenario when boxing happens is when value types like int
and DateTime
are passed as positional arguments to string
formats. This is a problem for Unity projects because they use a different memory garbage collector to the normal .NET runtime. For non-Unity projects, like all the projects in this book, you can ignore these boxing warnings because they are not relevant. You can read more about this Unity-specific issue at the following link: https://docs.unity3d.com/Manual/performance-garbage-collection-best-practices.html#boxing.
C# 6 and later have a handy feature named interpolated strings. A string
prefixed with $
can use curly braces around the name of a variable or expression to output the current value of that variable or expression at that position in the string
, as the following shows:
Program.cs
file, as shown in the following code:
// The following statement must be all on one line when using C# 10
// or earlier. If using C# 11 or later, we can include a line break
// in the middle of an expression but not in the string text.
Console.WriteLine($"{numberOfApples} apples cost {pricePerApple
* numberOfApples:C}");
12 apples cost £4.20
For short, formatted string
values, an interpolated string
can be easier for people to read. But for code examples in a book, where statements need to wrap over multiple lines, this can be tricky. For many of the code examples in this book, I will use numbered positional arguments. Another reason to avoid interpolated strings is that they can’t be read from resource files to be localized.
The next code example is not meant to be entered in your project.
Before C# 10, string
constants could only be combined by using concatenation with the +
operator, as shown in the following code:
private const string firstname = "Omar";
private const string lastname = "Rudberg";
private const string fullname = firstname + " " + lastname;
With C# 10, interpolated strings (prefixed with $
) can now be used, as shown in the following code:
private const string fullname = $"{firstname} {lastname}";
This only works for combining string
constant values. It cannot work with other types like numbers, which would require runtime data type conversions. You cannot enter private const
declarations in a top-level program like Program.cs
. You will see how to use them in Chapter 5, Building Your Own Types with Object-Oriented Programming.
Good Practice: If you are writing code that will be part of a Unity project, then interpolated string formats is an easy way to avoid boxing.
A variable or expression can be formatted using a format string after a comma or colon.
An N0
format string means a number with thousand separators and no decimal places, while a C
format string means currency. The currency format will be determined by the current thread.
For instance, if you run code that uses the number or currency format on a PC in the UK, you’ll get pounds sterling with commas as the thousand separators, but if you run it on a PC in Germany, you will get euros with dots as the thousand separators.
The full syntax of a format item is:
{ index [, alignment ] [ : formatString ] }
Each format item can have an alignment, which is useful when outputting tables of values, some of which might need to be left- or right-aligned within a width of characters. Alignment values are integers. Positive integers mean right-aligned and negative integers mean left-aligned.
For example, to output a table of fruit and how many of each there are, we might want to left-align the names within a column of 10 characters and right-align the counts formatted as numbers with zero decimal places within a column of six characters:
Program.cs
, enter the following statements:
string applesText = "Apples";
int applesCount = 1234;
string bananasText = "Bananas";
int bananasCount = 56789;
Console.WriteLine();
Console.WriteLine(format: "{0,-10} {1,6}",
arg0: "Name", arg1: "Count");
Console.WriteLine(format: "{0,-10} {1,6:N0}",
arg0: applesText, arg1: applesCount);
Console.WriteLine(format: "{0,-10} {1,6:N0}",
arg0: bananasText, arg1: bananasCount);
Name Count
Apples 1,234
Bananas 56,789
You can take complete control of number formatting using custom format codes, as shown in Table 2.8:
Format code |
Description |
|
Zero placeholder. Replaces the zero with the corresponding digit if present; otherwise, it uses zero. For example, |
|
Digit placeholder. Replaces the hash with the corresponding digit if present; otherwise, it uses nothing. For example, |
|
Decimal point. Sets the location of the decimal point in the number. Respects culture formatting, so it is a |
|
Group separator. Inserts a localized group separator between each group. For example, |
|
Percentage placeholder. Multiplies the value by 100 and adds a percentage character. |
|
Escape character. Makes the next character a literal instead of a format code. For example, |
|
Section separator. Defines different format strings for positive, negative, and zero numbers. For example, |
Others |
All other characters are shown in the output as is. |
Table 2.8: Custom numeric format codes
More Information: A full list of custom number format codes can be found at the following link: https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-numeric-format-strings.
You can apply standard number formatting using simpler format codes, like C
and N
. They support a precision number to indicate how many digits of precision you want. The default is two. The most common are, as shown in Table 2.9:
Format code |
Description |
|
Currency. For example, in US culture, |
|
Number. Integer digits with an optional negative sign and grouping characters. |
|
Decimal. Integer digits with an optional negative sign but no grouping characters. |
|
Binary. For example, |
|
Hexadecimal. For example, |
|
Exponential notation. For example, |
Table 2.9: Standard numeric format codes
More Information: A full list of standard number format codes can be found at the following link: https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings.
We can get text input from the user using the ReadLine
method. This method waits for the user to type some text. Then, as soon as the user presses Enter, whatever the user has typed is returned as a string
value.
Let’s get input from the user:
Console.Write("Type your first name and press ENTER: ");
string firstName = Console.ReadLine();
Console.Write("Type your age and press ENTER: ");
string age = Console.ReadLine();
Console.WriteLine($"Hello {firstName}, you look good for {age}.");
By default, with .NET 6 and later, nullability checks are enabled, so the C# compiler gives two warnings because the ReadLine
method could return a null
value instead of a string
value. But there is no scenario where this method would actually return null
, so instead we will see how to switch off these specific warnings in this scenario.
firstName
variable, append a ?
after string
, as shown highlighted in the following code:
string? firstName = Console.ReadLine();
This tells the compiler that we are expecting a possible null
value, so it does not need to warn us. If the variable is null
then when it is later output with WriteLine
, it will just be blank, so that works fine in this case. If we were going to access any of the members of the firstName
variable, then we would need to handle the case where it is null
.
age
variable, append a !
before the semi-colon at the end of the statement, as shown highlighted in the following code:
string age = Console.ReadLine()!;
This is called the null-forgiving operator because it tells the compiler that, in this case, ReadLine
will not return null
, so it can stop showing the warning. It is now our responsibility to ensure this is the case. Luckily, the Console
type’s implementation of ReadLine
always returns a string
even if it is just an empty string
value.
Type your name and press ENTER: Gary
Type your age and press ENTER: 34
Hello Gary, you look good for 34.
You have now seen two common ways to handle nullability warnings from the compiler. We will cover nullability and how to handle it in more detail in Chapter 6, Implementing Interfaces and Inheriting Classes.
In C# 6 and later, the using
statement can be used not only to import a namespace but also to further simplify our code by importing a static class. Then, we won’t need to enter the Console
type name throughout our code.
You can use your code editor’s Find and Replace feature to remove the times we have previously written Console
:
Program.cs
file, add a statement to statically import the System.Console
class, as shown in the following code:
using static System.Console;
Console.
in your code, ensuring that you select the dot after the word Console
too.Console.
with, as shown in Figure 2.9:Figure 2.9: Using the Replace feature in Visual Studio to simplify your code
Instead of statically importing the Console
class just for one code file, it would probably be better to import it globally for all code files in the project:
System.Console
.Formatting.csproj
, and after the <PropertyGroup>
section, add a new <ItemGroup>
section to globally and statically import System.Console
using the implicit using
.NET SDK feature, as shown in the following markup:
<ItemGroup>
<Using Include="System.Console" Static="true" />
</ItemGroup>
Good Practice: In the future, for all console app projects you create for this book, add the section above to simplify the code you need to write in all C# files to work with the Console
class.
We can get key input from the user using the ReadKey
method. This method waits for the user to press a key or key combination, which is then returned as a ConsoleKeyInfo
value.
Let’s explore reading key presses:
Write("Press any key combination: ");
ConsoleKeyInfo key = ReadKey();
WriteLine();
WriteLine("Key: {0}, Char: {1}, Modifiers: {2}",
arg0: key.Key, arg1: key.KeyChar, arg2: key.Modifiers);
Press any key combination: k
Key: K, Char: k, Modifiers: 0
Press any key combination: K
Key: K, Char: K, Modifiers: Shift
Press any key combination:
Key: F12, Char: , Modifiers: 0
Warning! When running a console app in a terminal within Visual Studio Code, some keyboard combinations will be captured by the code editor before they can be processed by your console app. For example, Ctrl + Shift + X in Visual Studio Code activates the Extensions view in the sidebar. To fully test this console app, open a command prompt or terminal in the project folder and run the console app from there.
When you run a console app, you often want to change its behavior by passing arguments. For example, with the dotnet
command-line tool, you can pass the name of a new project template, as shown in the following commands:
dotnet new console
dotnet new mvc
You might have been wondering how to get any arguments that might be passed to a console app.
In every version of .NET prior to version 6, the console app project template made it obvious, as shown in the following code:
using System;
namespace Arguments
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
The string[] args
arguments are declared and passed in the Main
method of the Program
class. They’re an array used to pass arguments into a console app. But in top-level programs, as used by the console app project template in .NET 6 and later, the Program
class and its Main
method are hidden, along with the declaration of the args
array. The trick is that you must know it still exists.
Command-line arguments are separated by spaces. Other characters like hyphens and colons are treated as part of an argument value.
To include spaces in an argument value, enclose the argument value in single or double quotes.
Imagine that we want to be able to enter the names of some colors for the foreground and background and the dimensions of the terminal window at the command line. We would be able to read the colors and numbers by reading them from the args
array, which is always passed into the Main
method, aka the entry point of a console app:
console
project named Arguments
to the Chapter02
solution.Arguments.csproj
, and after the <PropertyGroup>
section, add a new <ItemGroup>
section to statically import System.Console
for all C# files using the implicit usings .NET SDK feature, as shown in the following markup:
<ItemGroup>
<Using Include="System.Console" Static="true" />
</ItemGroup>
Good Practice: Remember to use the implicit usings .NET SDK feature to statically import the System.Console
type in all future console app projects to simplify your code, as these instructions will not be repeated every time.
Program.cs
, delete the existing statements, and then add a statement to output the number of arguments passed to the application, as shown in the following code:
WriteLine($"There are {args.Length} arguments.");
There are 0 arguments.
If you are using Visual Studio 2022:
firstarg second-arg third:arg "fourth arg"
, as shown in Figure 2.10:Figure 2.10: Entering command line arguments in the Visual Studio project properties on Windows
Properties
folder, open the launchSettings.json
file and note it defines the command-line arguments when you run the project, as shown highlighted in the following configuration:
{
"profiles": {
"Arguments": {
"commandName": "Project",
"commandLineArgs": "firstarg second-arg third:arg \"fourth arg\""
}
}
}
The launchSettings.json
file can also be used by JetBrains Rider. The equivalent for Visual Studio Code is the .vscode/launch.json
file.
If you are using JetBrains Rider:
firstarg second-arg third:arg "fourth arg"
, as shown in Figure 2.11:Figure 2.11: Entering command-line arguments in the JetBrains Rider run configuration
If you are using Visual Studio Code:
dotnet run
command, as shown in the following command:
dotnet run firstarg second-arg third:arg "fourth arg"
For all code editors:
There are 4 arguments.
Program.cs
, to enumerate or iterate (that is, loop through) the values of those four arguments, add the following statements after outputting the length of the array:
foreach (string arg in args)
{
WriteLine(arg);
}
There are 4 arguments.
firstarg
second-arg
third:arg
fourth arg
We will now use these arguments to allow the user to pick a color for the background, foreground, and cursor size of the output window. The cursor size can be an integer value from 1, meaning a line at the bottom of the cursor cell, up to 100, meaning a percentage of the height of the cursor cell.
We have statically imported the System.Console
class. It has properties like ForegroundColor
, BackgroundColor
, and CursorSize
that we can now set just by using their names without needing to prefix them with Console
.
The System
namespace is already imported so that the compiler knows about the ConsoleColor
and Enum
types:
if (args.Length < 3)
{
WriteLine("You must specify two colors and cursor size, e.g.");
WriteLine("dotnet run red yellow 50");
return; // Stop running.
}
ForegroundColor = (ConsoleColor)Enum.Parse(
enumType: typeof(ConsoleColor),
value: args[0], ignoreCase: true);
BackgroundColor = (ConsoleColor)Enum.Parse(
enumType: typeof(ConsoleColor),
value: args[1], ignoreCase: true);
CursorSize = int.Parse(args[2]);
Note the compiler warning that setting the CursorSize
is only supported on Windows. For now, do not worry about most of this code like (ConsoleColor)
, Enum.Parse
, or typeof
, as it will all be explained in the next few chapters.
red yellow 50
. Run the console app and note the cursor is half the size and the colors have changed in the window, as shown in Figure 2.12:Figure 2.12: Setting colors and cursor size on Windows
dotnet run red yellow 50
On macOS or Linux, you’ll see an unhandled exception, as shown in Figure 2.13:
Figure 2.13: An unhandled exception on unsupported macOS
Although the compiler did not give an error or warning, at runtime, some API calls may fail on some platforms. Although a console app running on Windows can change its cursor size, on macOS, it cannot, and it complains if you try.
So how do we solve this problem? We can solve this by using an exception handler. You will learn more details about the try
-catch
statement in Chapter 3, Controlling Flow, Converting Types, and Handling Exceptions, so for now, just enter the code:
try
statement, as shown in the following code:
try
{
CursorSize = int.Parse(args[2]);
}
catch (PlatformNotSupportedException)
{
WriteLine("The current platform does not support changing the size of the cursor.");
}
Another way to handle differences in operating systems is to use the OperatingSystem
class in the System
namespace, as shown in the following code:
if (OperatingSystem.IsWindows())
{
// Execute code that only works on Windows.
}
else if (OperatingSystem.IsWindowsVersionAtLeast(major: 10))
{
// Execute code that only works on Windows 10 or later.
}
else if (OperatingSystem.IsIOSVersionAtLeast(major: 14, minor: 5))
{
// Execute code that only works on iOS 14.5 or later.
}
else if (OperatingSystem.IsBrowser())
{
// Execute code that only works in the browser with Blazor.
}
The OperatingSystem
class has equivalent methods for other common operating systems, like Android, iOS, Linux, macOS, and even the browser, which is useful for Blazor web components.
A third way to handle different platforms is to use conditional compilation statements.
There are four preprocessor directives that control conditional compilation: #if
, #elif
, #else
, and #endif
.
You define symbols using #define
, as shown in the following code:
#define MYSYMBOL
Many symbols are automatically defined for you, as shown in Table 2.10:
Target Framework |
Symbols |
.NET Standard |
|
Modern .NET |
|
Table 2.10: Predefined compiler symbols
You can then write statements that will compile only for the specified platforms, as shown in the following code:
#if NET7_0_ANDROID
// Compile statements that only work on Android.
#elif NET7_0_IOS
// Compile statements that only work on iOS.
#else
// Compile statements that work everywhere else.
#endif
C# 5 introduced two C# keywords when working with the Task
type that enable easy multithreading. The pair of keywords is especially useful for the following:
In an online section, Building Websites Using the Model-View-Controller Pattern, we will see how the async
and await
keywords can improve scalability for websites. But for now, let’s see an example of how they can be used in a console app, and then later you will see them used in a more practical example within web projects.
One of the limitations with console apps is that you can only use the await
keyword inside methods that are marked as async
, but C# 7 and earlier do not allow the Main
method to be marked as async
! Luckily, a new feature introduced in C# 7.1 was support for async
in Main
.
Let’s see it in action:
console
project named AsyncConsole
to the Chapter02
solution.AsyncConsole.csproj
, and after the <PropertyGroup>
section, add a new <ItemGroup>
section to statically import System.Console
for all C# files using the implicit usings .NET SDK feature, as shown in the following markup:
<ItemGroup>
<Using Include="System.Console" Static="true" />
</ItemGroup>
Program.cs
, delete the existing statements, and then add statements to create an HttpClient
instance, make a request for Apple’s home page, and output how many bytes it has, as shown in the following code:
HttpClient client = new();
HttpResponseMessage response =
await client.GetAsync("http://www.apple.com/");
WriteLine("Apple's home page has {0:N0} bytes.",
response.Content.Headers.ContentLength);
In .NET 5 and earlier, you would have seen an error message, as shown in the following output:
Program.cs(14,9): error CS4033: The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'. [/Users/markjprice/Code/ Chapter02/AsyncConsole/AsyncConsole.csproj]
You would have had to add the async
keyword for your Main
method and change its return type from void
to Task
. With .NET 6 and later, the console app project template uses the top-level program feature to automatically define the Program
class with an asynchronous <Main>$
method for you.
Test your knowledge and understanding by answering some questions, getting some hands-on practice, and exploring the topics covered in this chapter with deeper research.
To get the best answer to some of these questions, you will need to do your own research. I want you to “think outside the book,” so I have deliberately not provided all the answers in the book.
I want to encourage you to get into the good habit of looking for help elsewhere, following the principle of “teach a person to fish.”
float
and double
values?double
uses in memory?var
keyword?XmlDocument
?dynamic
type?Appendix, Answers to the Test Your Knowledge Questions, is available to download from a link in the README on the GitHub repository: https://github.com/markjprice/cs12dotnet8.
What type would you choose for the following “numbers”?
In the Chapter02
solution, create a console app project named Ch02Ex03Numbers
that outputs the number of bytes in memory that each of the following number types uses and the minimum and maximum values they can have: sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, Int128
, UInt128
, Half
, float
, double
, and decimal
.
The result of running your console app should look something like Figure 2.14:
Figure 2.14: The result of outputting number type sizes
Code solutions for all exercises are available to download or clone from the GitHub repository at the following link: https://github.com/markjprice/cs12dotnet8.
Use the links on the following page to learn more details about the topics covered in this chapter:
https://github.com/markjprice/cs12dotnet8/blob/main/docs/book-links.md#chapter-2---speaking-c
No, not the villainous organization from the James Bond films! Spectre is a package that enhances console apps. You can read about it at the following link: https://spectreconsole.net/
.
In this chapter, you learned how to:
In the next chapter, you will learn about operators, branching, looping, converting between types, and how to handle exceptions.
Where there is an eBook version of a title available, you can buy it from the book details for that title. Add either the standalone eBook or the eBook and print book bundle to your shopping cart. Your eBook will show in your cart as a product on its own. After completing checkout and payment in the normal way, you will receive your receipt on the screen containing a link to a personalised PDF download file. This link will remain active for 30 days. You can download backup copies of the file by logging in to your account at any time.
If you already have Adobe reader installed, then clicking on the link will download and open the PDF file directly. If you don't, then save the PDF file on your machine and download the Reader to view it.
Please Note: Packt eBooks are non-returnable and non-refundable.
Packt eBook and Licensing When you buy an eBook from Packt Publishing, completing your purchase means you accept the terms of our licence agreement. Please read the full text of the agreement. In it we have tried to balance the need for the ebook to be usable for you the reader with our needs to protect the rights of us as Publishers and of our authors. In summary, the agreement says:
If you want to purchase a video course, eBook or Bundle (Print+eBook) please follow below steps:
Our eBooks are currently available in a variety of formats such as PDF and ePubs. In the future, this may well change with trends and development in technology, but please note that our PDFs are not Adobe eBook Reader format, which has greater restrictions on security.
You will need to use Adobe Reader v9 or later in order to read Packt's PDF eBooks.
Packt eBooks are a complete electronic version of the print edition, available in PDF and ePub formats. Every piece of content down to the page numbering is the same. Because we save the costs of printing and shipping the book to you, we are able to offer eBooks at a lower cost than print editions.
When you have purchased an eBook, simply login to your account and click on the link in Your Download Area. We recommend you saving the file to your hard drive before opening it.
For optimal viewing of our eBooks, we recommend you download and install the free Adobe Reader version 9.