Type Basics

A type defines the blueprint for a value. A value is a storage location denoted by a variable or a constant. A variable represents a value that can change, whereas a constant represents an invariant. We created a local variable named x in our first program:

	static void Main( )
	{
	  int x = 12 * 30;
	  Console.WriteLine (x);
	}

All values in C# are instances of a specific type. The meaning of a value, and the set of possible values a variable can have, is determined by its type. The type of x is int.

Predefined Type Examples

Predefined types are types that are specially supported by the compiler. The int type is a predefined primitive type for representing the set of integers that fits into 32 bits of memory, from –231 to 231–1. We can perform functions such as arithmetic with instances of the int type, as follows:

	int x = 12 * 30;

Another predefined C# type is the string type. The string type represents a sequence of characters, such as “.NET” or http://oreilly.com. We can manipulate strings by calling functions on them as follows:

	string message = "Hello world";
	string upperMessage = message.ToUpper( );
	Console.WriteLine (upperMessage);           // HELLO WORLD

	int x = 2007;
	message = message + x.ToString( );
	Console.WriteLine (message);             // Hello world2007

The primitive 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");

Note

In C#, predefined types (also referred to as built-in types) are recognized with a C# keyword. The System namespace in the .NET Framework contains many important types that C# does not predefine (e.g., DateTime).

Custom Type Examples

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)   // Constructor
	    { ratio = unitRatio; }

	  public int Convert (int unit)          // Method
	    { return unit * ratio; }
	}

	class Test
	{
	  static void Main( )
	  {
	    UnitConverter feetToInches = new UnitConverter(12);
	    UnitConverter milesToFeet = new UnitConverter(5280);

	    Console.Write (feetToInches.Convert(30));   // 360
	    Console.Write (feetToInches.Convert(100));  // 1200
	    Console.Write (feetToInches.Convert
	                    (milesToFeet.Convert(1)));  // 63360
	  }
	}

Members of a type

A type contains data members and function members. The data member of UnitConverter is the field called ratio. The function members of UnitConverter are the Convert method and the UnitConverter's constructor.

Symmetry of predefined types and custom types

A beautiful aspect of C# is that predefined types and custom types have few differences. The primitive 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.

Constructors and instantiation

Data is created by instantiating a type. Primitive types can be instantiated simply by using a literal. For example, the following line instantiates two integers (12 and 30), which are used to compute a third instance, x:

	int x = 12 * 30;

The new operator is needed to create a new instance 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 r)   // Constructor
	    { ratio = r; }
	  ...
	}

Instance versus static members

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, where all its members are static. You never actually create instances of a Console—one console is shared across the whole application.

To contrast instance versus static members, the instance field Name pertains to an instance of a particular Panda, whereas Population pertains to the set of all Panda instances:

	using System;

	public class Panda
	{
	  public string Name;              // Instance field
	  public static int Population;    // Static field

	  public Panda (string n)       // Constructor
	  {
	    Name = n;                   // Assign instance field
	    Population = Population+1;  // Increment static field
	  }
	}

The following code creates two instances of the Panda, prints their names and then the total population:

	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

The public keyword

The public keyword exposes members to other classes. In this example, if the Name field in Panda was not public, 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.

Conversions

C# can convert between instances of compatible types, through implicit and explicit conversions. 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 cast an int to a long type (which has twice the capacity of an int) and explicitly cast an int to a short type (which has half the capacity of an int):

	int x = 123456;      // int is a 32-bit integer
	long y = x;          // Implicit conversion to 64-bit int
	short z = (short)x;  // Explicit conversion to 16-bit int

Implicit conversions are allowed when:

  • The compiler can guarantee they will always succeed, and no information is lost in conversion.

Conversely, explicit conversions are required when:

  • The compiler cannot guarantee they will always succeed, or o information may be lost during conversion.

Most conversions are built into the language, such as the previously shown numeric conversions. Occasionally, it is useful to write custom conversions (see the upcoming “Operator Overloading” section).

Value Types Versus Reference Types

All C# types fall into the following categories:

  • Value types

  • Reference types

  • Pointer types

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.

The fundamental difference between value types and reference types is how they are handled in memory. Pointer types fall outside mainstream C# usage (see the upcoming “Unsafe Code and Pointers” section).

Value types

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 as follows (see Figure 1-1).

	public struct Point { public int X, Y; }
A value type instance in memory

Figure 1-1. A value type instance in memory

The assignment of a value type instance always copies the instance. For example:

	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 1-2 shows that p1 and p2 have independent storage.

Assignment copies a value type instance

Figure 1-2. Assignment copies a value type instance

Reference types

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 (seeFigure 1-3).

	public class Point { public int X, Y; }
A reference type instance in memory

Figure 1-3. A reference type instance in memory

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 X affects Y:

	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 1-4 shows that p1 and p2 are two references that point to the same object.

Assignment copies a reference

Figure 1-4. Assignment copies a reference

Null

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

Note

C# has a construct called nullable types for representing value-type nulls (see the later “Nullable Types” section).

Storage overhead

Value type instances occupy precisely the sum of the memory occupied by their fields.

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 (typically 12 bytes). Each reference to an object requires an extra 4 or 8 bytes, depending on whether the .NET runtime is running on a 32- or 64-bit platform.

Predefined Type Taxonomy

The following are the predefined types in C#:

Value types
  • Numeric types

    — Signed integer (sbyte, short, int, long)
    — Unsigned integer (byte, ushort, uint, ulong)
    — Real number (float, double, decimal)
  • Logical (bool)

  • Character (char)

Reference types
  • 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 predefined value types are also known as primitive types. Primitive types are so called because they are the atoms, or smallest possible building blocks of data, in a language, and most have a direct representation in machine code.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset