The type char
represents a 16-bit Unicode character. A character constant is
enclosed in single quotes, e.g. 'x'
or 'ř'.
There are 216 = 65,536 possible 16-bit values. Unfortunately, this is not enough to represent every possible Unicode character, since Unicode code points may be as large as 10FFFF16 = 1,114,11110. So not every Unicode character will fit in a C# char. This is somewhat unfortunate, and is partially due to the fact that Unicode had a smaller range of characters when C# was designed over 20 years ago.
Fortunately, most common
characters in living world languages have Unicode values that will
fit in 16 bits. However, emoji and other graphical characters may
not. For example, the tomato character (🍅) has a Unicode value of
1F34516,
which is larger than the largest possible 16-bit value FFFF16.
So it won't fit in a C# char:
char c = '🍅'; // error: too many characters in literal
However it can be stored in a C# string:
string t = "🍅";
This is a string of length 2, since it must be stored as two chars:
Console.WriteLine(t.Length); // writes 2
By contrast, Python has 32-bit characters, so this string would have length 1 in Python (which arguably makes more sense).
The type string
represents an immutable sequence
of characters. A string constant is enclosed in double quotes,
e.g. "hello".
We can access characters in a string using the [] operator:
string w = "watermelon"; char c = w[0]; // now c = 'w' char d = w[2]; // now c = 't'
An index preceded by ^ counts from the end of the string (like a negative index in Python):
char e = w[^1]; // now e = 'n'
It's possible to extract a range of characters from a string:
string w = "watermelon"; string x = w[2..5]; // now x = "ter" string y = w[5..]; // now y = "melon"
Notice that the range above includes all characters from the
character w[2] up to but not
including the character w[5].
(It's just like "w[2:5]" in
Python.)
The + operator
concatenates strings:
string s = "one two"; string t = "three four"; string u = s + " " + u; // "one two three four"
It may be suprising that you cannot compare strings using operators such as < and >:
if ("chameleon" < "dog") // ERROR: operator '<' can't be applied to strings
Console.WriteLine("less");
That's because the comparison order of strings may be
language-dependent. For example, "chameleon" appears before
"dog" in an English dictionary, but in a Czech dictionary
"chameleon" appears later since in Czech "ch" is
considered to be a single letter that comes after "h" in
the alphabet. So C# disallows this comparison. (Many other languages
such as Python are more liberal in this respect and will allow
strings to be compared using < or >.)
Every type in C# is either a value type or
a reference type. Of the types we have seen so far, value
types include all numeric types, bool,
and char. The only reference type we
have seen so far is string. (Soon we
will see many more.)
When a variable's type is a value type, the variable holds a value. An assignment between variables of value type copies the value from one variable to another:
int i = 10; int j = i; // i and j have independent copies of the value 10
When a variable's type is a reference type, the variable holds a reference to an object. An assignment between variables of reference type makes them refer to the same object:
string s = "antelope"; string t = s; // now s and t point to the same 8-character string in memory
C# includes a special value null,
which is like None in Python. It
represents the absence of a value.
Basic types cannot hold a null value:
int x = null; // error: cannot convert null to int
However, you can add the suffix ? to any
type to make it nullable. For example, int?
is a type that holds either an int, or null:
int? x = null; int? y = 7;
If we have a variable of a nullable type such as int?,
we must test whether its value is null before placing it into a
variable with a non-nullable type such as int.
For example:
int? i = ...;
if (i != null) {
int j = (int) i;
...
}
string? s = ...;
if (s != null) {
string t = s;
...
}
Notice above that we must use a type cast when converting int?
to int (even after we have checked for
null), but not when we convert string?
to string. More generally, the type cast
is necessary when converting T? to T for any value type T, but not
for reference types such as string.
(This is related to the fact that a conversion such as int?
to int actually changes the
representation of the data through a process known as unboxing.
There is no unboxing for reference types.)
If we omit the null checks above, the compiler will issue a warning:
int? i = ...; int j = (int) i; // warning: value may be null string? s = ...; string t = s; // warning: value may be null
As an alternative to checking for null explicitly, we can use the null-forgiving operator !, which throws an exception if a value is null, and otherwise produces the value unchanged:
int? i = ...; int j = (int) i!; // no warning, but will throw exception if null string? s = ...; string t = s!; // no warning, but will throw exception if null
The Console.ReadLine()
method reads a line of standard input. It returns a value of type
string?,
which is either a string, or is null
(in the case that no more input is available).
So we can write
string? s = Console.ReadLine();
if (s == null)
Console.WriteLine("no more input");
else
Console.WriteLine(s);Often we don't want to worry about the null case, so to keep things simple we use the null-forgiving operator:
string s = Console.ReadLine()!;
We have learned about various built-in types in C#
including int, uint,
long, ulong,
bool, char,
and string. Actually each of these names
is an abbreviation for a longer name:
int =
System.Int32
uint =
System.UInt32
long =
System.Int64
ulong =
System.UInt64
bool =
System.Boolean
char =
System.Char
string =
System.String
You may use either the short or long names in code:
int i = 44; System.Int32 i = 44; // equivalent
Our C# quick reference is organized using the long names, and lists some useful constants and methods for these types.
For example, the section System.Int32
lists constants and methods related to the int
type. The MinValue and MaxValue
constants may be useful:
int min = int.MinValue; // smallest possible int int max = int.MaxValue; // largest possible int
The quick reference also lists some helpful methods for converting between strings and integers:
FormatException
if the string does not contain a valid integer.
Notice that Parse() is a static
method. That means that we invoke it on the int
type directly:
int x = int.Parse("345");
ToString() is not marked with static,
so it is an instance method. We invoke it on an instance
of the int type, i.e. an integer itself:
int i = 345; string s = i.ToString();
Later in this course we'll see how to write instance methods and static methods in our own classes.
A string beginning with $
may contain interpolated values
enclosed in braces (just like f-strings in Python):
int a = 3, b = 4;
Console.WriteLine($"{a} plus {b} equals {a + b}");A interpolated value may be followed by a format specifier (preceded by a colon).
There are many predefined format specifiers. Many format specifiers can be followed by an integer called the precision, whose meaning varies among specifiers. Here are two useful specifiers:
f – prints a
number in fixed-point decimal (i.e. not using scientific notation).
The precision is the number of the digits after the decimal point.
n – prints a
number using thousands separators. The precision is the number of
digits after the decimal point.
For example,
const double pi = 3.14159;
const int i = 1357;
Console.WriteLine($"{pi:f2} {i:n0}");writes
3.14 1,357
Some more format specifiers are listed in our C# quick reference.
A foreach statement
iterates over each element of a collection:
foreach (char c in "hello")
Console.WriteLine(c);The only kinds of collections we have learned about so far are strings (but we will soon see others).
This statement is similar to the for
statement in Python, which can also loop over various kinds of
collections.
A foreach statement
always declares an iteration variable whose scope is the body of the
statement.
We very often want to read input from files. Our
class library reference lists some methods
that make that fairly easy. First, the static method ReadLines()
returns an enumeration of all lines in a file. You can loop over
these lines with foreach. For example, here is a program that reads
all lines of a file nums.txt that
contains one integer per line, and prints their sum:
int sum = 0;
foreach (string line in File.ReadLines("nums.txt"))
sum += int.Parse(line);
Console.WriteLine(sum);Also, the static method ReadAllText() will read the entire text of a file into a string:
string book = File.ReadAllText("war_and_peace.txt");
However, as I pointed out in Programming 1 it's often more efficient to process a file line by line rather than reading its entire contents into a string.
A do/while
loop is similar to a while loop, but
checks the loop condition at the bottom of the loop body. For
example:
string? s;
do {
s = ReadLine();
} while (s != "yes" && s != "no");
Note that the body of an ordinary while
loop might not execute at all, but the body of a do/while
loop will always execute at least once.
In most programs do/while
loops are less common than ordinary while
loops, but they are still occasionally useful.
C# includes fixed-size arrays. You can allocate a single-dimensional array like this:
int[] a = new int[10]; // an array that holds 10 integers
When you allocate an array using the new
operator, all elements will be initialized to the element type's
default value. The default value for int
and other numeric types is 0. The default value for bool
is false.
The default value of every reference type (such as
string) is null. And so if you create an
array of any reference type, all its elements will initially be null.
That is true even if the type is not nullable! For example:
string[] a = new string[10]; // array of non-nullable strings string?[] b = new string[10]; // array of nullable strings
The elements in both of these arrays will initially be null. However
you cannot assign null to any element of a:
a[3] = null; // ERROR: converting null to non-nullable type
Arguably this is an inconsistency in the language. It exists for
historical reasons. In older versions of C# there were no nullable
types, and a value of any reference type could be null.
When nullable types were added to C# a few years ago, the default
value of reference types remained null,
presumably for backward compatibility.
You may declare an array and initialize its elements to specific values:
int[] a = [ 3, 4, 5 ];
Arrays are indexed from 0, and the ^ operator indexes from the end:
int x = a[1]; // now x is 4 int y = a[^1]; // now y is 5, the last value in the array
An array in C# is a reference type, so more than one variable may refer to the same array. For example:
int[] a = [ 10, 20, 30, 40 ];
int[] b = a;
a[0] = 100;
WriteLine(b[0]); // writes 100
We see that the assignment "int[] b = a"
does not copy the array.
Instead, it sets b to point to the same array as a. (We also saw this
phenomenon in Python.)
You can access ranges of arrays just like for strings:
int[] a = [ 2, 4, 6, 8, 10 ];
int[] c = a[1..4]; // { 4, 6, 8 }
int[] d = a[..3]; // { 2, 4, 6 }
int[] e = a[3..]; // { 8, 10 }Note that the array range operator makes a copy of a range of elements (just like slice operators in Python). Of course, it follows that creating a range of N elements will take O(N) time.
The Length property
returns the length of an array:
int y = a.Length;
An array is a collection, so you can loop over it with foreach:
foreach (int i in a)
Console.WriteLine(i);
Because arrays have a fixed size, you cannot append a value to an
existing array. If you want to append values, probably you should use
a List, i.e. a dynamic array (we'll
discuss these soon).
The + operator does not append arrays in C#. However, you can use a collection expression to achieve the same effect:
int[] a = [10, 20, 30]; int[] b = [50, 60, 70]; int[] c = [..a, ..b]; // append a and b
More generally, a collection expression can contain individual
elements and/or spread elements such as "..a"
that include all the elements from an existing collection. So could
write e.g.
int[] a = [10, 20, 30]; int[] b = [5, ..a, 100, ..a, 200];
After the declarations above, b will be [5, 10,
20, 30, 100, 10, 20, 30, 200].
In C# you can create two kinds of multidimensional arrays: rectangular arrays and jagged arrays.
You can dynamically allocate a rectangular
array using the new operator:
int[,] a =newint[3,4];
Or you can allocate an array and initialize it with a set of values:
int[,] a = { {1, 4, 5}, {2, 3, 6} };Note that you must use braces, not brackets, when specifying initial values for a rectangular array.
You can
access an element of a rectangular array using an expression such as
a[2,
5].
An rectangular array may even have 3 or more dimensions, e.g.
int[,,] a = new int[5, 10, 15]; // a 3-dimensional array
In a rectangular array a, a.Length
is the total number of elements in the
array. This is the product of the lengths of all dimensions.
a.Rank is the number of dimensions, and
the GetLength() method returns the
length of a given
dimension (numbered from 0):
int[,,] a = new int[5, 10, 15]; // a 3-dimensional array Console.WriteLine(a.Length); // writes 750 (= 5 * 10 * 15) Console.WriteLine(a.Rank); // writes 3 Console.WriteLine(a.GetLength(1)); // writes 10
Length and Rank
are properties, not methods, so when we access them we don't
write (). A property looks like a field (i.e. an attribute), but is
not exactly the same as a field since code may run when a property is
retrieved. We'll study properties in more detail later in this
course, and will learn how to implement properties in our own
classes.
A jagged array is an array of arrays. Unlike in a rectangular multidimensional array, each subarray in a jagged array can have a different length.
You can allocate a jagged array like this:
int[][] a = new int[3][]; a[0] = new int[2]; a[1] = new int[4]; a[2] = new int[5];
Alternatively, you can initialize a jagged array as you allocate it:
int[][] a = [[2, 3], [3, 4, 5], [1, 3, 5, 7, 9]];
You can access an element of a jagged array using an expression such
as a[2][5].
A dynamic array is an extremely useful data type.
The C# standard library includes a type List<T>,
which implements a dynamic array. This is just like a list in Python.
The T in List<T>
indicates that this is a generic type. We'll study generics in
more detail later in this course, and will learn how to implement our
own generic types. For now, we only need to understand that T
represents any type. We may construct a List<int>,
which holds integers, a List<string>
which holds strings, or a list of any other type. Note that (unlike
in Python) all elements of a list must have a common type.
The List<T>
class is in the System.Collections.Generic
namespace. As we saw last week, in a new C# project the implicit
usings feature is enabled by default. With implicit usings, this
namespace is automatically imported, so you can use List<T>
without writing a "using" statement.
However, we also learned last week that implicit
usings are not enabled on ReCodEx. So if you want to use List<T>
in a program that you submit to ReCodEx, you'll need to import its
namespace explicitly:
using System.Collections.Generic;
When we create a list, we may specify initial elements using the same bracket syntax that we used for arrays:
List<int>a=[3,5,7];
As another possibility, we can create an empty list and add integers to it in a loop:
List<int> a = [];
for (int i = 0 ; i < 10 ; ++i)
a.Add(i * i);The .Add() method is just like .append() in Python: it appends an element to a list, and will run in O(1) on average.
Once we have a list, we may get or set its elements by index:
a[0] = a[1] + a[2]; WriteLine(a[2]);
The Count property retrieves the number of elements in a List<T>:
WriteLine($"a has {a.Count} elements");
Collection expressions work with List
objects just as with arrays. For example, we may write
List<int> a = [10, 20, 30]; List<int> b = [100, 200, 300]; List<int> c = [..a, ..b]; // concatenate two lists List<int> d = [0, ..a, 0];
We may even use collection expressions to convert between arrays and lists:
int[] a = [2, 4, 6, 8, 10]; List<int> b = [..a]; // convert array to List int[] c = [..b]; // convert List to array
Our C# quick reference lists
more useful methods on List objects. If you look at the documentation
for System.Collections.Generic
in the quick reference, you'll see that List<T>
inherits from IList<T>, which
inherits from ICollection<T>,
which inherits from IEnumerable<T>.
So you'll find methods that work on List
objects not only in the section about List<T>,
but also in the sections about those supertypes as well. For example,
the Insert() and RemoveAt()
methods are listed in the documentation for IList<T>,
and will work on any list. We'll study inheritance and C#'s
collection class hierarchy in more detail in a later lecture.