A C# Program Is a Set of Type Declarations
Data Members and Function Members
Value Types and Reference Types
Static Typing and the dynamic Keyword
If you were to broadly characterize the source code of C and C++ programs, you might say that a C program is a set of functions and data types and that a C++ program is a set of functions and classes. A C# program, however, is a set of type declarations.
Main
.For example, the following code shows a program that consists of three type declarations. The three types are declared inside a new namespace called MyProgram
.
namespace MyProgram // Create a new namespace.
{
DeclarationOfTypeA // Declare a type.
DeclarationOfTypeB // Declare a type.
class C // Declare a type.
{
static void Main()
{
...
}
}
}
Namespaces are covered in more detail in Chapter 10.
Since a C# program is just a set of type declarations, learning C# consists of learning how to create and use types. So, the first thing you need to do is to look at what a type is.
You can start by thinking of a type as a template for creating data structures. It isn't the data structure itself, but it specifies the characteristics of objects constructed from the template.
A type is defined by the following elements:
For example, Figure 3-1 illustrates the components of two types: short
and int
.
Figure 3-1. A type is a template.
Creating an actual object from the type's template is called instantiating the type.
Figure 3-2 illustrates the instantiation of objects of two predefined types.
Figure 3-2. Instantiating a type creates an instance.
Some types, such as short
, int
, and long
, are called simple types and can store only a single data item.
Other types can store multiple data items. An array, for example, is a type that can store multiple items of the same type. The individual items are called elements and are referenced by a number, called an index. Chapter 14 describes arrays in detail.
Other types, however, can contain data items of many different types. The individual elements in these types are called members, and, unlike arrays, in which each member is referred to by a number, these members have distinct names.
There are two types of members: data members and function members.
For example, Figure 3-3 shows some of the data members and function members of type XYZ
. It contains two data members and two function members.
Figure 3-3. Types specify data members and function members.
C# provides 16 predefined types, which are shown in Figure 3-4 and listed in Tables 3-1 and 3-2. They include 13 simple types and 3 nonsimple types.
The names of all the predefined types consist of all lowercase characters. The predefined simple types include the following:
float
and double
.decimal
. Unlike float
and double
, type decimal
can represent decimal fractional numbers exactly. It's often used for monetary calculations.char
.bool
. Type bool
represents Boolean values and must be one of two values—either true
or false
.
Note Unlike C and C++, numeric values do not have a Boolean interpretation in C#.
The three nonsimple types are the following:
string
, which is an array of Unicode charactersobject
, which is the type on which all other types are baseddynamic
, which is used when using assemblies written in dynamic languagesFigure 3-4. The predefined types
All the predefined types are mapped directly to underlying .NET types. The C# type names are just aliases for the .NET types, so using the .NET names works fine syntactically, although this is discouraged. Within a C# program, you should use the C# names rather than the .NET names.
The predefined simple types represent a single item of data. They're listed in Table 3-1, along with the ranges of values they can represent and the underlying .NET types to which they map.
Table 3-1. The Predefined Simple Types
Name | Meaning | Range | .NET Framework Type |
Default Value |
sbyte |
8-bit signed integer | -128–127 | System.SByte |
0 |
byte |
8-bit unsigned integer | 0–255 | System.Byte |
0 |
short |
16-bit signed integer | -32,768–32,767 | System.Int16 |
0 |
ushort |
16-bit unsigned integer | 0–65,535 | System.UInt16 |
0 |
int |
32-bit signed integer | -2,147,483,648–2,147,483,647 | System.Int32 |
0 |
uint |
32-bit unsigned integer | 0–4,294,967,295 | System.UInt32 |
0 |
long |
64-bit signed integer | -9,223,372,036,854,775,808–9,223,372,036,854,775,807 | System.Int64 |
0 |
ulong |
64-bit unsigned integer | 0–18,446,744,073,709,551,615 | System.UInt64 |
0 |
float |
Single-precision float | 1.5×10-45–3.4×1038 | System.Single |
0.0f |
double |
Double-precision float | 5×10-324–1.7×10308 | System.Double |
0.0d |
bool |
Boolean | true , false |
System.Boolean |
false |
char |
Unicode character | U+0000–U+ffff | System.Char |
x0000 |
decimal |
Decimal value with 28-significant-digit precision | ± 1.0×1028–±7.9×1028 | System.Decimal |
0m |
The nonsimple predefined types are somewhat more complex. Values of type string
contain zero or more Unicode characters. The object
type is the base class for all other types in the system, including the predefined, simple types. Table 3-2 shows the predefined nonsimple types.
Table 3-2. The Predefined Nonsimple Types
Name | Meaning | .NET Framework Type |
object |
The base class from which all other types are derived | System.Object |
string |
A sequence of Unicode characters | System.String |
dynamic |
A type designed to be used with assemblies written in dynamic languages |
Besides the 15 predefined types provided by C#, you can also create your own user-defined types. There are six kinds of types you can create. They are the following:
class
typesstruct
typesarray
typesenum
typesdelegate
typesinterface
typesYou create a type using a type declaration, which includes the following information:
array
and delegate
types, which don't have named membersOnce you've declared a type, you can create and use objects of the type just as if they were predefined types. Figure 3-5 summarizes the use of predefined and user-defined types. Using predefined types is a one-step process in which you simply instantiate the objects of that type. Using user-defined types is a two-step process. You must first declare the type and then instantiate objects of the type.
Figure 3-5. The predefined types require instantiation only. The user-defined types require two steps: declaration and instantiation.
While a program is running, its data must be stored in memory. How much memory is required for an item, and where and how it's stored, depends on its type.
A running program uses two regions of memory to store data: the stack and the heap.
The system takes care of all stack manipulation. You, as the programmer, don't need to do anything with it explicitly. But understanding its basic functions will give you a better understanding of what your program is doing when it's running and allow you to better understand the C# documentation and literature.
The stack is an array of memory that acts as a last-in, first-out (LIFO) data structure. It stores several types of data:
The general characteristics of stacks are the following:
Figure 3-6 illustrates the functions and terminology of the stack.
Figure 3-6. Pushing and popping on the stack
The heap is an area where chunks of memory are allocated to store certain kinds of data objects. Unlike the stack, memory can be stored and removed from the heap in any order. Figure 3-7 shows a program that has stored four items in the heap.
Figure 3-7. The memory heap
Although your program can store items in the heap, it cannot explicitly delete them. Instead, the CLR's garbage collector (GC) automatically cleans up orphaned heap objects when it determines that your code is no longer accessing them. This frees you from what in other programming languages can be an error-prone task. Figure 3-8 illustrates the garbage collection process.
Figure 3-8. Automatic garbage collection in the heap
The type of a data item defines how much memory is required to store it and the data members that comprise it. The type also determines where an object is stored in memory—the stack or the heap.
Types are divided into two categories: value types and reference types. Objects of these types are stored differently in memory.
Data that is not a member of another type is stored as shown in Figure 3-9. For value types, data is stored on the stack. For reference types, the actual data is stored in the heap, and the reference is stored on the stack.
Figure 3-9. Storing data that is not part of another type
Figure 3-9 shows how data is stored when it isn't a member of another type. When it's a member of another type, data might be stored a little differently.
Suppose, for example, that you have an instance of a reference type, called MyType
, that has two members—a value type member and a reference type member. How is it stored? Is the value type member stored on the stack and the reference type split between the stack and the heap, as shown in Figure 3-9? The answer is no.
Remember that for a reference type, the data of an instance is always stored in the heap. Since both members are part of the object's data, they're both stored in the heap, regardless of whether they are value or reference types. Figure 3-10 illustrates the case of type MyType
.
A
is a value type, it's part of the data of the instance of MyType
and is therefore stored with the object's data in the heap.B
is a reference type, and therefore its data portion will always be stored in the heap, as shown by the small box marked “Data.” What's different is that its reference is also stored in the heap, inside the data portion of the enclosing MyType
object.Figure 3-10. Storage of data as part of a reference type
Note For any object of a reference type, all its data members are stored in the heap, regardless of whether they are of value type or reference type.
Table 3-3 shows all the types available in C# and what kinds of types they are—value types or reference types. Each reference type is covered later in the text.
Table 3-3. Value Types and Reference Types in C#
Value Types | Reference Types | |||
Predefined types | sbyte short int long bool |
byte ushort uint ulong |
float double char decimal |
object string dynamic |
User-defined types | struct enum |
class interface delegate array |
A general-purpose programming language must allow a program to store and retrieve data.
Table 3-4. The Four Kinds of Variables
Name | Member of a Type | Description |
Local variable | No | Holds temporary data within the scope of a method |
Field | Yes | Holds data associated with a type or an instance of a type |
Parameter | No | A temporary variable used to pass data from one method to another method |
Array element | Yes | One member of a sequenced collection of (usually) homogeneous data items |
A variable must be declared before it can be used. The variable declaration defines the variable and accomplishes two things:
A simple variable declaration requires at least a type and a name. The following declaration defines a variable named var2
, of type int
:
For example, Figure 3-11 represents the declaration of four variables and their places on the stack.
Figure 3-11. Value type and reference type variable declarations
Besides declaring a variable's name and type, you can optionally use the declaration to initialize its memory to a specific value.
A variable initializer consists of an equals sign followed by the initializing value, as shown here:
Local variables without initializers have an undefined value and cannot be used until they have been assigned a value. Attempting to use an undefined local variable causes the compiler to produce an error message.
Figure 3-12 shows a number of local variable declarations on the left and the resulting stack configuration on the right. Some of the variables have initializers, and others do not.
Figure 3-12. Variable initializers
Some kinds of variables are automatically set to default values if they are declared without an initializer, and others are not. Variables that are not automatically initialized to default values contain undefined values until the program assigns them a value. Table 3-5 shows which types of variables are automatically initialized and which are not. I'll cover each of the five variable types later in the text.
Variable Type | Stored In | Auto-initialized | Use |
Local variables | Stack or stack and heap | No | Used for local computation inside a function member |
Class fields | Heap | Yes | Members of a class |
Struct fields | Stack or heap | Yes | Members of a struct |
Parameters | Stack | No | Used for passing values into and out of a method |
Array elements | Heap | Yes | Members of an array |
You can declare multiple variables in a single declaration statement.
For example, the following code shows two valid declaration statements with multiple variables. Notice that the initialized variables can be mixed with uninitialized variables as long as they're separated by commas. The last declaration statement shown is invalid because it attempts to declare different types of variables in a single statement.
A variable name represents the value stored by the variable. You can use the value by using the variable name.
For example, in the following statement, the value of var2
is retrieved from memory and placed at the position of the variable.
Console.WriteLine("{0}", var2);
One thing you'll have noticed is that every variable includes the type of the variable, allowing the compiler to determine the amount of memory it will require at runtime and which parts should be stored in the stack and which in the heap. The type of the variable is determined at compile time and cannot be changed at runtime. This is called static typing.
Not all languages, though, are statically typed. Many, including such scripting languages as IronPython and IronRuby, are dynamically typed. That is, the type of a variable might not be resolved until runtime. Since these are .NET languages, C# programs need to be able to use assemblies written in these languages.
To solve the problem that C# needs to be able to resolve at compile time a type referenced in an assembly that doesn't resolve its types until runtime, the C# language designers added the keyword dynamic
to the language. The dynamic
keyword represents a specific, actual C# type that knows how to resolve itself at runtime. That is, it's statically typed as dynamic!
This satisfies both constraints. The C# compiler can resolve the keyword to an actual type, and the type object can resolve itself to the target assembly's type at runtime.
There are situations, particularly when working with databases, where you want to indicate that a variable does not currently hold a valid value. For reference types, you can do this easily, by setting the variable to null
. When you define a variable of a value type, however, its memory is allocated whether or not its contents have any valid meaning.
What you would like in this situation is to have a Boolean indicator associated with the variable, so that when the value is valid, the indicator is true
, and when the value is not valid, the indicator is false
.
Nullable types allow you to create a value type variable that can be marked as valid or invalid so that you can make sure a variable is valid before using it. Regular value types are called non-nullable types.
A nullable type is always based on another type, called the underlying type, that has already been declared.
To create a variable of a nullable type, simply add a question mark to the end of the name of the underlying type, in the variable declaration. Unfortunately, this syntax makes it appear that you have a lot of questions about your code.
For example, the following code declares a variable of the nullable int
type. Notice that the suffix is attached to the type name—not the variable name.
With this declaration statement, the compiler takes care of both producing the nullable type and creating the variable of that type.
Using a nullable type is almost the same as using a variable of any other type. Reading a variable of a nullable type returns its value. You must, however, make sure that the variable is not null
. Attempting to read the value of a null
variable produces an exception.
null
.Both sets of code produce the following output:
15
You can easily convert between a nullable type and its corresponding non-nullable type. We'll go into conversions in detail in Chapter 18, but the important points for nullable types are the following:
For example, the following lines show conversion in both directions. In the first line, a literal of type int
is implicitly converted to a value of type int?
and is used to initialize the variable of the nullable type. In the second line, the variable is explicitly converted to its non-nullable version.
int? myInt1 = 15; // Implicitly convert int to int?
int regInt = (int) myInt1; // Explicitly convert int? to int
You can assign three kinds of values to a variable of a nullable type:
null
The following code shows an example of each of the three types of assignment:
int? myI1, myI2, myI3;
myI1 = 28; // Value of underlying type
myI2 = myI1; // Value of nullable type
myI3 = null; // Null
Console.WriteLine("myI1: {0}, myI2: {1}", myI1, myI2);
This code produces the following output:
myI1: 28, myI2: 28
In Chapter 25, when you have a clearer understanding of C#, I'll explain the finer points of nullable types.
3.145.107.100