The conditional operator (?
:) is similar to the if
statement, but it is an expression, not a statement. It takes
a boolean value plus two extra values. It returns the first of these
values if the boolean is true, or the second if it is false.
For example, the following statement computes the greater of i and j and assigns it to k:
int i, j; ... int k = i > j ? i : j; // set k to the maximum of i and j
This operator is also sometimes called the ternary operator since it takes three operands. We also saw this operator in Python, which uses a completely different syntax for it:
# Python code: set k to the maximum of i and j k = i if i > j else j;
In Python, we used the chr()
and ord() functions to convert numeric
codes into characters and vice versa. In C# there are no such
functions. Instead, we can accomplish the same task through a simple
assignment.
You can implicitly convert a char
to any numeric type that can hold a 16-bit unsigned value:
char c = 'ř'; int i = c; // now i holds the Unicode value for 'ř', i.e. 345
You can explicitly convert any numeric type to a char:
int i = 100; char c = (char) i; // now c is 'd', which is ASCII/Unicode character 100
A tuple holds two or more values. Unlike
arrays, tuples have fixed length and can hold values of varying
types. We write tuples using a syntax that's just like Python: for
example, (3,
"hello",
false)
is a tuple containing an int, a
string, and a bool.
We can assign tuples to variables in several different ways that may look superficially similar. First, we may unpack each element of a tuple into a separate variable:
(int x, int y) = (10, 20);
The code above produces separate variables x and y. We can also perform a tuple assignment into existing variables:
(x, y) = (30, 40); // assign 30 to x, and assign 40 to y (x, y) = (y, x); // swap x and y
Alternatively, we may assign an entire tuple to a single variable:
(int, int) p = (10, 20); (int, string) q = (3, "hello");
Above, (int, int) and (int,
string) are tuple types. A tuple type is written as a
list of element types in parentheses.
We can access a tuple's elements using the names
Item1, Item2
and so on:
(int, int) p = (10, 20); WriteLine(p.Item1); // writes 10 WriteLine(p.Item2); // writes 20
The names Item1 and Item2
are pretty ugly. Alternatively, we can specify our own names inside
a tuple type:
(int x, int y) p = (10, 20);
This is called a named tuple. We can now access the tuple elements by name:
WriteLine($"x = {p.x}, y = {p.y}");This looks quite a bit nicer.
Be sure to note the difference between the syntax
for multiple assignment and the syntax for creating a named tuple –
these may look similar at first. The declaration above creates a
single variable p with fields p.x
and p.y. By contrast, the line
(int x, int y) = (10, 20);
is a multiple assigment that
creates local variables x and y.
You can use any of the assignment forms above in a
foreach loop. For example:
(int, int)[] points = [ (2, 3), (5, 7), (8, 9) ];
foreach ((int x, int y) in points) // assign to variables x and y
WriteLine($"{x}, {y}");
foreach ((int, int) p in points)
WriteLine($"{p.Item1}, {p.Item2}"); // a bit ugly
foreach ((int x, int y) p in points) // assign to a named tuple
WriteLine($"{p.x}, {p.y}");The choice between these is a matter of style.
It can be awkward to write named tuple types repeatedly in code:
(int x, int y) p = (10, 20); int z = p.x + p.y; (int x, int y) q = p;
As an alternative to this, you can create a type alias via a
using statement at the top of your
source file:
using Point = (int x, int y);
With this alias, we can write the code above more concisely:
Point p = (10, 20); int z = p.x + p.y; Point q = p;
A function can take one or more parameters, each with a specific type. It also has a return type. For example:
double abc(double d, int a) {
return d / (2 * a);
}
// Compute the sum of all values in an array of ints.
int sum(int[] a) {
int s = 0;
foreach (int i in a)
s += i;
return s;
}
The return statement returns a value
from a function, and may be called anywhere from within the function
body (just like in Python). The first function above returns a
double, and the second returns an int.
If a function's return type is void,
then it does not return a value:
void countdown(int i) {
while (i > 0) {
Console.WriteLine(i);
--i;
}
}Note that ReCodEx will not accept code with top-level functions, i.e. functions that are outside of any class. If you write code with top-level functions and want to submit it to ReCodEx, you will need to move the functions inside a class by making them be static methods of the class. (As we'll see in a future lecture, a static method is something like a function that belongs to a class.) So you could write e.g.
using System;
class Top {
static void countdown(int i) {
while (i > 0) {
Console.WriteLine(i);
--i;
}
}
static void Main() {
countdown(10);
}
}We have seen basic syntax for defining functions in C#:
int mul(int x, int y) {
return x * y;
}The function above just returns an expression. C# lets us define functions such as this using a compact syntax:
int mul(int x, int y) => x * y;
This is called an expression-bodied function. I recommend using this syntax when possible.
A
parameter marked with out
returns a value to the caller. For example, this function takes two
integers and returns their quotient as an ordinary return value. It
also returns the remainder via an out
parameter:
int divide(int a, int b, out int remainder) {
remainder = a % b;
return a / b;
}
You must include the keyword out when
passing a variable to an out parameter:
int q, r; q = divide(3, 4, out r);
You can declare a new variable as you invoke a method with an out
parameter. The following is equivalent to the two preceding lines:
int q = divide(3, 4, out int r);
A
parameter marked with ref
is passed by reference: the method can modify the variable that is
passed. For example, here's a function that swaps two integer
variables:
void swap(ref int a, ref int b) {
int t = a;
a = b;
b = t;
}
You must include the keyword ref when
passing an argument by reference:
int i = 3, j = 4; swap(ref i, ref j); WriteLine(i); // writes 4
If the last parameter to a function is marked with
the keyword params and has an array type
T[], then in its place the function can receive any number of
arguments of type T. The caller may pass individual values separated
by commas, or may pass an array of type T[].
For example, this function computes the sum of any number of integers:
int sum(params int[] a) {
int s = 0;
foreach (int i in a)
s += i;
return s;
}It can be invoked as
int s = sum(4, 5, 6);
Alternatively, the caller can pass an array:
int[] a = [ 4, 5, 6 ]; int s = sum(a);
A class is a user-defined data type that can contain fields, constructors, methods, and other kinds of members.
As a first example, here's a simple class implementing a point in three dimensions:
class Point {
public double x, y, z; // fields
// constructor
public Point(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
}
public bool is_zero() =>
x == 0.0 && y == 0.0 && z == 0.0;
}We may create a Point like this:
Point p = new(3.0, 4.0, 6.0); WriteLine(p.is_zero()); // will write "false"
Each instance of a class
contains a set of fields.
A field may be declared with an initial value:
class Foo {
int x = 3, y = 4;
…
}
If you don't specify an initial value, a field will intially be set
to its type's default value (e.g 0 for an int).
In the Point class above, is_zero()
is a method. We write methods using the same syntax that we
previously saw for functions.
Every member in a class can
be either public
or private.
public
members are accessible everywhere, and private
members are accessible only within the class.
The default access level is private.
A constructor makes a new instance of a class. It always has the same name as its containing class.
A constructor will often intialize fields, as in the Point class above.
If a class definition includes no constructors, then C# provides a default constructor that is public and takes no arguments.
this
is a special value that refers to the object on which a constructor
or method was invoked. (It's like the self
variable in Python.) In code, you may access a member x
of the containing class by writing either this.x
or simply x.
For example, in the method is_zero()
in the Point class above we could have written
this.x==0.0&&this.y==0.0&&this.z==0.0;
but instead we just referred to the fields x, y, and z directly.
(This is a significant difference from Python, in which we must
always write self.x to access the
attribute x.)
On the other hand, in
the Point() constructor above we had to
write this.x to access the field x,
since the constructor also has an parameter named x. (If we wrote "x
= x" that would assign the parameter to itself, which
would do nothing.)
Here's a class implementing a fixed-size stack:
class Stack {
// fields
int[] a;
int count;
// constructor
public Stack(int max_size) {
a = new int[max_size];
count = 0;
}
// methods
public bool is_empty() =>
count == 0;
public void push(int i) {
a[count] = i;
count += 1;
}
public int pop() {
count -= 1;
return a[count];
}
}
Notice that in this class the fields are private. Usually it's good
programming practice to make fields private, except in classes whose
only purpose is to hold data (such as the Point
class above). Then the caller can only modify the class's state by
calling its public interface.