In this chapter, we introduce the basics of the C# language.
All programs and code snippets in this and the following two chapters are available as interactive samples in LINQPad. Working through these samples in conjunction with the book accelerates learning in that you can edit the samples and instantly see the results without needing to set up projects and solutions in Visual Studio.
To download the samples, go to LINQPad’s Sample Libraries page and choose “C# 6.0 in a Nutshell.” LINQPad is free—go to http://www.linqpad.net.
Here is a program that multiplies 12 by 30 and prints the result, 360, to the screen. The double forward slash indicates that the remainder of a line is a comment:
using System; // Importing namespace class Test // Class declaration { static void Main() // Method declaration { int x = 12 * 30; // Statement 1 Console.WriteLine (x); // Statement 2 } // End of method } // End of class
At the heart of this program lie two statements:
int x = 12 * 30; Console.WriteLine (x);
Statements in C# execute sequentially and are terminated by a semicolon (or a code block, as we’ll see later). The first statement computes the expression 12 * 30
and stores the result in a local variable, named x
, which is an integer type. The second statement calls the Console
class’s WriteLine
method, to print the variable x
to a text window on the screen.
A method performs an action in a series of statements, called a statement block—a pair of braces containing zero or more statements. We defined a single method named Main
:
static void Main() { ... }
Writing higher-level functions that call upon lower-level functions simplifies a program. We can refactor our program with a reusable method that multiplies an integer by 12 as follows:
using System; class Test { static void Main() { Console.WriteLine (FeetToInches (30)); // 360 Console.WriteLine (FeetToInches (100)); // 1200 } static int FeetToInches (int feet) { int inches = feet * 12; return inches; } }
A method can receive input data from the caller by specifying parameters and output data back to the caller by specifying a return type. We defined a method called FeetToInches
that has a parameter for inputting feet, and a return type for outputting inches:
static int FeetToInches (int feet ) {...}
The literals 30
and 100
are the arguments passed to the FeetToInches
method. The Main
method in our example has empty parentheses because it has no parameters, and is void
because it doesn’t return any value to its caller:
static void Main()
C# recognizes a method called Main
as signaling the default entry point of execution. The Main
method may optionally return an integer (rather than void
) in order to return a value to the execution environment (where a nonzero value typically indicates an error). The Main
method can also optionally accept an array of strings as a parameter (that will be populated with any arguments passed to the executable). For example:
static int Main (string[] args) {...}
An array (such as string[]
) represents a fixed number of elements of a particular type. Arrays are specified by placing square brackets after the element type and are described in “Arrays”.
Methods are one of several kinds of functions in C#. Another kind of function we used in our example program was the *
operator, which performs multiplication. There are also constructors, properties, events, indexers, and finalizers.
In our example, the two methods are grouped into a class. A class groups function members and data members to form an object-oriented building block. The Console
class groups members that handle command-line input/output functionality, such as the WriteLine
method. Our Test
class groups two methods—the Main
method and the FeetToInches
method. A class is a kind of type, which we will examine in “Type Basics”.
At the outermost level of a program, types are organized into namespaces. The using
directive was used to make the System
namespace available to our application, to use the Console
class. We could define all our classes within the TestPrograms
namespace, as follows:
using System; namespace TestPrograms { class Test {...} class Test2 {...} }
The .NET Framework is organized into nested namespaces. For example, this is the namespace that contains types for handling text:
using System.Text;
The using
directive is there for convenience; you can also refer to a type by its fully qualified name, which is the type name prefixed with its namespace, such as System.Text.StringBuilder
.
The C# compiler compiles source code, specified as a set of files with the .cs extension, into an assembly. An assembly is the unit of packaging and deployment in .NET. An assembly can be either an application or a library. A normal console or Windows application has a Main
method and is an .exe file. A library is a .dll and is equivalent to an .exe without an entry point. Its purpose is to be called upon (referenced) by an application or by other libraries. The .NET Framework is a set of libraries.
The name of the C# compiler is csc.exe. You can either use an IDE such as Visual Studio to compile, or call csc
manually from the command line. (The compiler is also available as a library; see Chapter 27.) To compile manually, first save a program to a file such as MyFirstProgram.cs, and then go to the command line and invoke csc
(located in %ProgramFiles(X86)%msbuild14.0in) as follows:
csc MyFirstProgram.cs
This produces an application named MyFirstProgram.exe.
Peculiarly, .NET Framework 4.6 ships with the C# 5 compiler. To obtain the C# 6 command-line compiler, you must install Visual Studio or MSBuild 14.
To produce a library (.dll), do the following:
csc /target:library MyFirstProgram.cs
We explain assemblies in detail in Chapter 18.
C# syntax is inspired by C and C++ syntax. In this section, we will describe C#’s elements of syntax, using the following program:
using System; class Test { static void Main() { int x = 12 * 30; Console.WriteLine (x); } }
Identifiers are names that programmers choose for their classes, methods, variables, and so on. These are the identifiers in our example program, in the order they appear:
System Test Main x Console WriteLine
An identifier must be a whole word, essentially made up of Unicode characters starting with a letter or underscore. C# identifiers are case-sensitive. By convention, parameters, local variables, and private fields should be in camel case (e.g., myVariable
), and all other identifiers should be in Pascal case (e.g., MyMethod
).
Keywords are names that mean something special to the compiler. These are the keywords in our example program:
using class static void int
Most keywords are reserved, which means that you can’t use them as identifiers. Here is the full list of C# reserved keywords:
abstract as base bool break byte case catch char checked class const continue decimal default delegate |
do double else enum event explicit extern false finally fixed float for foreach goto if implicit |
in int interface internal is lock long namespace new null object operator out override params private |
protected public readonly ref return sbyte sealed short sizeof stackalloc static string struct switch this throw |
true try typeof uint ulong unchecked unsafe ushort using virtual void volatile while |
If you really want to use an identifier that clashes with a reserved keyword, you can do so by qualifying it with the @
prefix. For instance:
class class {...} // Illegal class @class {...} // Legal
The @
symbol doesn’t form part of the identifier itself. So @myVariable
is the same as myVariable
.
The @
prefix can be useful when consuming libraries written in other .NET languages that have different keywords.
Some keywords are contextual, meaning they can also be used as identifiers—without an @
symbol. These are:
add ascending async await by descending |
dynamic equals from get global group |
into join let nameof on orderby |
partial remove select set value var |
when where yield |
With contextual keywords, ambiguity cannot arise within the context in which they are used.
Literals are primitive pieces of data lexically embedded into the program. The literals we used in our example program are 12
and 30
.
Punctuators help demarcate the structure of the program. These are the punctuators we used in our example program:
{ } ;
The braces group multiple statements into a statement block.
The semicolon terminates a statement. (Statement blocks, however, do not require a semicolon.) Statements can wrap multiple lines:
Console.WriteLine (1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10);
An operator transforms and combines expressions. Most operators in C# are denoted with a symbol, such as the multiplication operator, *
. We will discuss operators in more detail later in this chapter. These are the operators we used in our example program:
. () * =
A period denotes a member of something (or a decimal point with numeric literals). Parentheses are used when declaring or calling a method; empty parentheses are used when the method accepts no arguments. (Parentheses also have other purposes that we’ll see later in this chapter.) An equals sign performs assignment. (The double equals sign, ==
, performs equality comparison, as we’ll see later.)
C# offers two different styles of source-code documentation: single-line comments and multiline comments. A single-line comment begins with a double forward slash and continues until the end of the line. For example:
int x = 3; // Comment about assigning 3 to x
A multiline comment begins with /*
and ends with */
. For example:
int x = 3; /* This is a comment that spans two lines */
Comments may embed XML documentation tags, explained in “XML Documentation” in Chapter 4.
A type defines the blueprint for a value. In our example, we used two literals of type int
with values 12 and 30. We also declared a variable of type int
whose name was x
:
static void Main() { int x = 12 * 30; Console.WriteLine (x); }
A variable denotes a storage location that can contain different values over time. In contrast, a constant always represents the same value (more on this later):
const int y = 360;
All values in C# are instances of a type. The meaning of a value, and the set of possible values a variable can have, is determined by its type.
Predefined types are types that are specially supported by the compiler. The int
type is a predefined type for representing the set of integers that fit into 32 bits of memory, from −231 to 231−1, and is the default type for numeric literals within this range. We can perform functions such as arithmetic with instances of the int
type as follows:
int x = 12 * 30;
Another predefined C# type is string
. The string
type represents a sequence of characters, such as “.NET” or “http://oreilly.com.” We can work with strings by calling functions on them as follows:
string message = "Hello world"; string upperMessage = message.ToUpper(); Console.WriteLine (upperMessage); // HELLO WORLD int x = 2015; message = message + x.ToString(); Console.WriteLine (message); // Hello world2015
The predefined bool
type has exactly two possible values: true
and false
. The bool
type is commonly used to conditionally branch execution flow based with an if
statement. For example:
bool simpleVar = false; if (simpleVar) Console.WriteLine ("This will not print"); int x = 5000; bool lessThanAMile = x < 5280; if (lessThanAMile) Console.WriteLine ("This will print");
Just as we can build complex functions from simple functions, we can build complex types from primitive types. In this example, we will define a custom type named UnitConverter
—a class that serves as a blueprint for unit conversions:
using System; public class UnitConverter { int ratio; // Field public UnitConverter (int unitRatio) {ratio = unitRatio; } // Constructor public int Convert (int unit) {return unit * ratio; } // Method } class Test { static void Main() { UnitConverter feetToInchesConverter = new UnitConverter (12); UnitConverter milesToFeetConverter = new UnitConverter (5280); Console.WriteLine (feetToInchesConverter.Convert(30)); // 360 Console.WriteLine (feetToInchesConverter.Convert(100)); // 1200 Console.WriteLine (feetToInchesConverter.Convert( milesToFeetConverter.Convert(1))); // 63360 } }
A beautiful aspect of C# is that predefined types and custom types have few differences. The predefined int
type serves as a blueprint for integers. It holds data—32 bits—and provides function members that use that data, such as ToString
. Similarly, our custom UnitConverter
type acts as a blueprint for unit conversions. It holds data—the ratio—and provides function members to use that data.
Data is created by instantiating a type. Predefined types can be instantiated simply by using a literal such as 12
or "Hello world"
. The new
operator creates instances of a custom type. We created and declared an instance of the UnitConverter
type with this statement:
UnitConverter feetToInchesConverter = new UnitConverter (12);
Immediately after the new
operator instantiates an object, the object’s constructor is called to perform initialization. A constructor is defined like a method, except that the method name and return type are reduced to the name of the enclosing type:
public class UnitConverter { ... public UnitConverter (int unitRatio) { ratio = unitRatio; } ... }
The data members and function members that operate on the instance of the type are called instance members. The UnitConverter
’s Convert
method and the int
’s ToString
method are examples of instance members. By default, members are instance members.
Data members and function members that don’t operate on the instance of the type, but rather on the type itself, must be marked as static
. The Test.Main
and Console.WriteLine
methods are static methods. The Console
class is actually a static class, which means all its members are static. You never actually create instances of a Console
—one console is shared across the whole application.
Let’s contrast instance from static members. In the following code, the instance field Name
pertains to an instance of a particular Panda
, whereas Population
pertains to the set of all Panda
instances:
public class Panda { public string Name; // Instance field public static int Population; // Static field public Panda (string n) // Constructor { Name = n; // Assign the instance field Population = Population + 1; // Increment the static Population field } }
The following code creates two instances of the Panda
, prints their names, and then prints the total population:
using System; class Test { static void Main() { Panda p1 = new Panda ("Pan Dee"); Panda p2 = new Panda ("Pan Dah"); Console.WriteLine (p1.Name); // Pan Dee Console.WriteLine (p2.Name); // Pan Dah Console.WriteLine (Panda.Population); // 2 } }
Attempting to evaluate p1.Population
or Panda.Name
will generate a compile-time error.
The public
keyword exposes members to other classes. In this example, if the Name
field in Panda
was not marked as public, it would be private, and the Test
class could not access it. Marking a member public
is how a type communicates: “Here is what I want other types to see—everything else is my own private implementation details.” In object-oriented terms, we say that the public members encapsulate the private members of the class.
C# can convert between instances of compatible types. A conversion always creates a new value from an existing one. Conversions can be either implicit or explicit: implicit conversions happen automatically, and explicit conversions require a cast. In the following example, we implicitly convert an int
to a long
type (which has twice the bitwise capacity of an int
) and explicitly cast an int
to a short
type (which has half the capacity of an int
):
int x = 12345; // int is a 32-bit integer long y = x; // Implicit conversion to 64-bit integer short z = (short)x; // Explicit conversion to 16-bit integer
Implicit conversions are allowed when both of the following are true:
The compiler can guarantee they will always succeed.
No information is lost in conversion.1
Conversely, explicit conversions are required when one of the following is true:
The compiler cannot guarantee they will always succeed.
Information may be lost during conversion.
(If the compiler can determine that a conversion will always fail, both kinds of conversion are prohibited. Conversions that involve generics can also fail in certain conditions—see “Type Parameters and Conversions” in Chapter 3.)
The numeric conversions that we just saw are built into the language. C# also supports reference conversions and boxing conversions (see Chapter 3) as well as custom conversions (see “Operator Overloading” in Chapter 4). The compiler doesn’t enforce the aforementioned rules with custom conversions, so it’s possible for badly designed types to behave otherwise.
All C# types fall into the following categories:
Value types
Reference types
Generic type parameters
Pointer types
In this section, we’ll describe value types and reference types. We’ll cover generic type parameters in “Generics” in Chapter 3, and pointer types in “Unsafe Code and Pointers” in Chapter 4.
Value types comprise most built-in types (specifically, all numeric types, the char
type, and the bool
type), as well as custom struct
and enum
types.
Reference types comprise all class, array, delegate, and interface types. (This includes the predefined string
type.)
The fundamental difference between value types and reference types is how they are handled in memory.
The content of a value type variable or constant is simply a value. For example, the content of the built-in value type, int
, is 32 bits of data.
You can define a custom value type with the struct
keyword (see Figure 2-1):
public struct Point { public int X; public int Y; }
or more tersely:
public struct Point { public int X, Y; }
The assignment of a value-type instance always copies the instance. For example:
static void Main() { Point p1 = new Point(); p1.X = 7; Point p2 = p1; // Assignment causes copy Console.WriteLine (p1.X); // 7 Console.WriteLine (p2.X); // 7 p1.X = 9; // Change p1.X Console.WriteLine (p1.X); // 9 Console.WriteLine (p2.X); // 7 }
Figure 2-2 shows that p1
and p2
have independent storage.
A reference type is more complex than a value type, having two parts: an object and the reference to that object. The content of a reference-type variable or constant is a reference to an object that contains the value. Here is the Point
type from our previous example rewritten as a class, rather than a struct
(shown in Figure 2-3):
public class Point { public int X, Y; }
Assigning a reference-type variable copies the reference, not the object instance. This allows multiple variables to refer to the same object—something not ordinarily possible with value types. If we repeat the previous example, but with Point
now a class, an operation to p1
affects p2
:
static void Main() { Point p1 = new Point(); p1.X = 7; Point p2 = p1; // Copies p1 reference Console.WriteLine (p1.X); // 7 Console.WriteLine (p2.X); // 7 p1.X = 9; // Change p1.X Console.WriteLine (p1.X); // 9 Console.WriteLine (p2.X); // 9 }
Figure 2-4 shows that p1
and p2
are two references that point to the same object.
A reference can be assigned the literal null
, indicating that the reference points to no object:
class Point {...} ... Point p = null; Console.WriteLine (p == null); // True // The following line generates a runtime error // (a NullReferenceException is thrown): Console.WriteLine (p.X);
In contrast, a value type cannot ordinarily have a null value:
struct Point {...} ... Point p = null; // Compile-time error int x = null; // Compile-time error
C# also has a construct called nullable types for representing value-type nulls (see “Nullable Types” in Chapter 4).
Value-type instances occupy precisely the memory required to store their fields. In this example, Point
takes eight bytes of memory:
struct Point { int x; // 4 bytes int y; // 4 bytes }
Technically, the CLR positions fields within the type at an address that’s a multiple of the fields’ size (up to a maximum of eight bytes). Thus, the following actually consumes 16 bytes of memory (with the seven bytes following the first field “wasted”):
struct A { byte b; long l; }
You can override this behavior with the StructLayout
attribute (see “Mapping a Struct to Unmanaged Memory” in Chapter 25).
Reference types require separate allocations of memory for the reference and object. The object consumes as many bytes as its fields, plus additional administrative overhead. The precise overhead is intrinsically private to the implementation of the .NET runtime, but at minimum, the overhead is eight bytes, used to store a key to the object’s type, as well as temporary information such as its lock state for multithreading and a flag to indicate whether it has been fixed from movement by the garbage collector. Each reference to an object requires an extra four or eight bytes, depending on whether the .NET runtime is running on a 32- or 64-bit platform.
The predefined types in C# are:
Numeric
—Signed integer (sbyte
, short
, int
, long
)
—Unsigned integer (byte
, ushort
, uint
, ulong
)
—Real number (float
, double
, decimal
)
Logical (bool
)
Character (char
)
String (string
)
Object (object
)
Predefined types in C# alias Framework types in the System
namespace. There is only a syntactic difference between these two statements:
int i = 5; System.Int32 i = 5;
The set of predefined value types, excluding decimal
, are known as primitive types in the CLR. Primitive types are so called because they are supported directly via instructions in compiled code, and this usually translates to direct support on the underlying processor. For example:
// Underlying hexadecimal representation int i = 7; // 0x7 bool b = true; // 0x1 char c = 'A'; // 0x41 float f = 0.5f; // uses IEEE floating-point encoding
The System.IntPtr
and System.UIntPtr
types are also primitive (see Chapter 25).
C# has the predefined numeric types shown in Table 2-1.
C# type | System type | Suffix | Size | Range |
---|---|---|---|---|
Integral—signed | ||||
sbyte |
SByte |
8 bits | –27 to 27–1 | |
short |
Int16 |
16 bits | –215 to 215–1 | |
int |
Int32 |
32 bits | –231 to 231–1 | |
long |
Int64 |
L |
64 bits | –263 to 263–1 |
Integral—unsigned | ||||
byte |
Byte |
8 bits | 0 to 28–1 | |
ushort |
UInt16 |
16 bits | 0 to 216–1 | |
uint |
UInt32 |
U |
32 bits | 0 to 232–1 |
ulong |
UInt64 |
UL |
64 bits | 0 to 264–1 |
Real | ||||
float |
Single |
F |
32 bits | ± (~10–45 to 1038) |
double |
Double |
D |
64 bits | ± (~10–324 to 10308) |
decimal |
Decimal |
M |
128 bits | ± (~10–28 to 1028) |
Of the integral types, int
and long
are first-class citizens and are favored by both C# and the runtime. The other integral types are typically used for interoperability or when space efficiency is paramount.
Of the real number types, float
and double
are called floating-point types2 and are typically used for scientific and graphical calculations. The decimal
type is typically used for financial calculations, where base-10-accurate arithmetic and high precision are required.
Integral literals can use decimal or hexadecimal notation; hexadecimal is denoted with the 0x
prefix. For example:
int x = 127; long y = 0x7F;
Real literals can use decimal and/or exponential notation. For example:
double d = 1.5; double million = 1E06;
By default, the compiler infers a numeric literal to be either double
or an integral type:
If the literal contains a decimal point or the exponential symbol (E
), it is a double
.
Otherwise, the literal’s type is the first type in this list that can fit the literal’s value: int
, uint
, long
, and ulong
.
For example:
Console.WriteLine ( 1.0.GetType()); // Double (double) Console.WriteLine ( 1E06.GetType()); // Double (double) Console.WriteLine ( 1.GetType()); // Int32 (int) Console.WriteLine ( 0xF0000000.GetType()); // UInt32 (uint) Console.WriteLine (0x100000000.GetType()); // Int64 (long)
Numeric suffixes explicitly define the type of a literal. Suffixes can be either lower- or uppercase, and are as follows:
Category | C# type | Example |
---|---|---|
F |
float |
float f = 1.0F; |
D |
double |
double d = 1D; |
M |
decimal |
decimal d = 1.0M; |
U |
uint |
uint i = 1U; |
L |
long |
long i = 1L; |
UL |
ulong |
ulong i = 1UL; |
The suffixes U
and L
are rarely necessary, because the uint
, long
, and ulong
types can nearly always be either inferred or implicitly converted from int
:
long i = 5; // Implicit lossless conversion from int literal to long
The D
suffix is technically redundant, in that all literals with a decimal point are inferred to be double
. And you can always add a decimal point to a numeric literal:
double x = 4.0;
The F
and M
suffixes are the most useful and should always be applied when specifying float
or decimal
literals. Without the F
suffix, the following line would not compile, because 4.5 would be inferred to be of type double
, which has no implicit conversion to float
:
float f = 4.5F;
The same principle is true for a decimal literal:
decimal d = -1.23M; // Will not compile without the M suffix.
We describe the semantics of numeric conversions in detail in the following section.
Integral conversions are implicit when the destination type can represent every possible value of the source type. Otherwise, an explicit conversion is required. For example:
int x = 12345; // int is a 32-bit integral long y = x; // Implicit conversion to 64-bit integral short z = (short)x; // Explicit conversion to 16-bit integral
A float
can be implicitly converted to a double
, since a double
can represent every possible value of a float
. The reverse conversion must be explicit.
All integral types may be implicitly converted to all floating-point types:
int i = 1; float f = i;
The reverse conversion must be explicit:
int i2 = (int)f;
When you cast from a floating-point number to an integral, any fractional portion is truncated; no rounding is performed. The static class System.Convert
provides methods that round while converting between various numeric types (see Chapter 6).
Implicitly converting a large integral type to a floating-point type preserves magnitude but may occasionally lose precision. This is because floating-point types always have more magnitude than integral types, but may have less precision. Rewriting our example with a larger number demonstrates this:
int i1 = 100000001; float f = i1; // Magnitude preserved, precision lost int i2 = (int)f; // 100000000
The increment and decrement operators (++
, --
) increment and decrement numeric types by 1. The operator can either follow or precede the variable, depending on whether you want its value before or after the increment/decrement. For example:
int x = 0, y = 0; Console.WriteLine (x++); // Outputs 0; x is now 1 Console.WriteLine (++y); // Outputs 1; y is now 1
Division operations on integral types always truncate remainders (round toward zero). Dividing by a variable whose value is zero generates a runtime error (a DivideByZeroException
):
int a = 2 / 3; // 0 int b = 0; int c = 5 / b; // throws DivideByZeroException
Dividing by the literal or constant 0 generates a compile-time error.
At runtime, arithmetic operations on integral types can overflow. By default, this happens silently—no exception is thrown, and the result exhibits “wraparound” behavior, as though the computation was done on a larger integer type and the extra significant bits discarded. For example, decrementing the minimum possible int
value results in the maximum possible int
value:
int a = int.MinValue; a--; Console.WriteLine (a == int.MaxValue); // True
The checked
operator tells the runtime to generate an OverflowException
rather than overflowing silently when an integral expression or statement exceeds the arithmetic limits of that type. The checked
operator affects expressions with the ++
, −−
, +
, −
(binary and unary), *
, /
, and explicit conversion operators between integral types.
The checked
operator has no effect on the double
and float
types (which overflow to special “infinite” values, as we’ll see soon) and no effect on the decimal
type (which is always checked).
checked
can be used around either an expression or a statement block. For example:
int a = 1000000; int b = 1000000; int c = checked (a * b); // Checks just the expression. checked // Checks all expressions { // in statement block. ... c = a * b; ... }
You can make arithmetic overflow checking the default for all expressions in a program by compiling with the /checked+
command-line switch (in Visual Studio, go to Advanced Build Settings). If you then need to disable overflow checking just for specific expressions or statements, you can do so with the unchecked
operator. For example, the following code will not throw exceptions—even if compiled with /checked+
:
int x = int.MaxValue; int y = unchecked (x + 1); unchecked { int z = x + 1; }
The 8- and 16-bit integral types are byte
, sbyte
, short
, and ushort
. These types lack their own arithmetic operators, so C# implicitly converts them to larger types as required. This can cause a compile-time error when trying to assign the result back to a small integral type:
short x = 1, y = 1; short z = x + y; // Compile-time error
In this case, x
and y
are implicitly converted to int
so that the addition can be performed. This means the result is also an int
, which cannot be implicitly cast back to a short
(because it could cause loss of data). To make this compile, we must add an explicit cast:
short z = (short) (x + y); // OK
Unlike integral types, floating-point types have values that certain operations treat specially. These special values are NaN (not a number), +∞, −∞, and −0. The float
and double
classes have constants for NaN
, +∞, and −∞, as well as other values (MaxValue
, MinValue
, and Epsilon
). For example:
Console.WriteLine (double.NegativeInfinity); // -Infinity
The constants that represent special values for double
and float
are as follows:
Special value | Double constant | Float constant |
---|---|---|
NaN | double.NaN |
float.NaN |
+∞ | double.PositiveInfinity |
float.PositiveInfinity |
−∞ | double.NegativeInfinity |
float.NegativeInfinity |
−0 | −0.0 |
−0.0f |
Dividing a nonzero number by zero results in an infinite value. For example:
Console.WriteLine ( 1.0 / 0.0); // Infinity Console.WriteLine (−1.0 / 0.0); // -Infinity Console.WriteLine ( 1.0 / −0.0); // -Infinity Console.WriteLine (−1.0 / −0.0); // Infinity
Dividing zero by zero, or subtracting infinity from infinity, results in a NaN. For example:
Console.WriteLine ( 0.0 / 0.0); // NaN Console.WriteLine ((1.0 / 0.0) − (1.0 / 0.0)); // NaN
When using ==
, a NaN value is never equal to another value, even another NaN value:
Console.WriteLine (0.0 / 0.0 == double.NaN); // False
To test whether a value is NaN, you must use the float.IsNaN
or double.IsNaN
method:
Console.WriteLine (double.IsNaN (0.0 / 0.0)); // True
When using object.Equals
, however, two NaN values are equal:
Console.WriteLine (object.Equals (0.0 / 0.0, double.NaN)); // True
NaNs are sometimes useful in representing special values. In WPF, double.NaN
represents a measurement whose value is “Automatic”. Another way to represent such a value is with a nullable type (Chapter 4); another is with a custom struct that wraps a numeric type and adds an additional field (Chapter 3).
float
and double
follow the specification of the IEEE 754 format types, supported natively by almost all processors. You can find detailed information on the behavior of these types at http://www.ieee.org.
double
is useful for scientific computations (such as computing spatial coordinates). decimal
is useful for financial computations and values that are “man-made” rather than the result of real-world measurements. Here’s a summary of the differences:
Category | double |
decimal |
---|---|---|
Internal representation | Base 2 | Base 10 |
Decimal precision | 15–16 significant figures | 28–29 significant figures |
Range | ±(~10−324 to ~10308) | ±(~10−28 to ~1028) |
Special values | +0, −0, +∞, −∞, and NaN | None |
Speed | Native to processor | Non-native to processor (about 10 times slower than double ) |
float
and double
internally represent numbers in base 2. For this reason, only numbers expressible in base 2 are represented precisely. Practically, this means most literals with a fractional component (which are in base 10) will not be represented precisely. For example:
float tenth = 0.1f; // Not quite 0.1 float one = 1f; Console.WriteLine (one - tenth * 10f); // -1.490116E-08
This is why float
and double
are bad for financial calculations. In contrast, decimal
works in base 10 and so can precisely represent numbers expressible in base 10 (as well as its factors, base 2 and base 5). Since real literals are in base 10, decimal
can precisely represent numbers such as 0.1. However, neither double
nor decimal
can precisely represent a fractional number whose base 10 representation is recurring:
decimal m = 1M / 6M; // 0.1666666666666666666666666667M double d = 1.0 / 6.0; // 0.16666666666666666
This leads to accumulated rounding errors:
decimal notQuiteWholeM = m+m+m+m+m+m; // 1.0000000000000000000000000002M double notQuiteWholeD = d+d+d+d+d+d; // 0.99999999999999989
which breaks equality and comparison operations:
Console.WriteLine (notQuiteWholeM == 1M); // False Console.WriteLine (notQuiteWholeD < 1.0); // True
C#’s bool
type (aliasing the System.Boolean
type) is a logical value that can be assigned the literal true
or false
.
Although a Boolean value requires only one bit of storage, the runtime will use one byte of memory, since this is the minimum chunk that the runtime and processor can efficiently work with. To avoid space inefficiency in the case of arrays, the Framework provides a BitArray
class in the System.Collections
namespace that is designed to use just one bit per Boolean value.
==
and !=
test for equality and inequality of any type, but always return a bool
value.3 Value types typically have a very simple notion of equality:
int x = 1; int y = 2; int z = 1; Console.WriteLine (x == y); // False Console.WriteLine (x == z); // True
For reference types, equality, by default, is based on reference, as opposed to the actual value of the underlying object (more on this in Chapter 6):
public class Dude { public string Name; public Dude (string n) { Name = n; } } ... Dude d1 = new Dude ("John"); Dude d2 = new Dude ("John"); Console.WriteLine (d1 == d2); // False Dude d3 = d1; Console.WriteLine (d1 == d3); // True
The equality and comparison operators, ==
, !=
, <
, >
, >=
, and <=
, work for all numeric types, but should be used with caution with real numbers (as we saw in “Real-Number Rounding Errors”). The comparison operators also work on enum
type members, by comparing their underlying integral values. We describe this in “Enums” in Chapter 3.
We explain the equality and comparison operators in greater detail in “Operator Overloading” in Chapter 4, and in “Equality Comparison” and “Order Comparison” in Chapter 6.
The &&
and ||
operators test for and and or conditions. They are frequently used in conjunction with the !
operator, which expresses not. In this example, the UseUmbrella
method returns true
if it’s rainy or sunny (to protect us from the rain or the sun), as long as it’s not also windy (since umbrellas are useless in the wind):
static bool UseUmbrella (bool rainy, bool sunny, bool windy) { return !windy && (rainy || sunny); }
The &&
and ||
operators short-circuit evaluation when possible. In the preceding example, if it is windy, the expression (rainy || sunny)
is not even evaluated. Short-circuiting is essential in allowing expressions such as the following to run without throwing a NullReferenceException
:
if (sb != null && sb.Length > 0) ...
The &
and |
operators also test for and and or conditions:
return !windy & (rainy | sunny);
The difference is that they do not short-circuit. For this reason, they are rarely used in place of conditional operators.
Unlike in C and C++, the &
and |
operators perform (non-short-circuiting) Boolean comparisons when applied to bool
expressions. The &
and |
operators perform bitwise operations only when applied to numbers.
The conditional operator (more commonly called the ternary operator, as it’s the only operator that takes three operands) has the form q ? a : b
, where if condition q
is true, a
is evaluated, else b
is evaluated. For example:
static int Max (int a, int b) { return (a > b) ? a : b; }
The conditional operator is particularly useful in LINQ queries (Chapter 8).
C#’s char
type (aliasing the System.Char
type) represents a Unicode character and occupies 2 bytes. A char
literal is specified inside single quotes:
char c = 'A'; // Simple character
Escape sequences express characters that cannot be expressed or interpreted literally. An escape sequence is a backslash followed by a character with a special meaning. For example:
char newLine = ' '; char backSlash = '';
The escape sequence characters are shown in Table 2-2.
Char | Meaning | Value |
---|---|---|
' |
Single quote | 0x0027 |
" |
Double quote | 0x0022 |
\ |
Backslash | 0x005C |
|