Chapter 4

 

Types

 

Type Declaration

 

A declaration creates a name in a declaration space. The only declaration that can have the same name is a constructor, method, indexer or an operator. These are the only entities that can be overloaded in a type. We cannot have a field and a method with the same name in a declaration space. Each time we create a new class, struct or interface, a new declaration space is created.

 

The only entities in a class/struct that can share the same name as the class/struct is a instance constructor or a static constructor. Base class and derived classes live in separate declaration space, hence the derived class can hide names created in the base class. An enum also creates its own declaration space.

 

A local variable declaration space is created by the {} braces or a block. The textual order of names created is of no importance at all except in certain situations. The initialization of the variables is carried out in the order they have been created. Local variables must be defined prior to using them in the remaining program. The enums declaration order is only important when constant expression values are not used.

 

There are five different ways to create our own types. These are class, struct, interface, enum or a delegate. These type declarations can occur in three places only. The usual suspects are namespaces, struct and the obvious classes.

 

The compiler, basically, understands only two types, viz, value types and reference types. It can seamlessly convert any value type into a reference type or an object. This conversion of value to and from reference types is given a name, boxing and unboxing.

 

A value type can be one of the two, either a structure or an enum. The simple data types are predefined structures further divided into numeric, integer or floating point types. We have nine integral types sbyte, byte, short, ushort, int, uint, long, ulong and char and two floating point types float and double.

 

a.cs

class zzz : int

{

}

 

Compiler Error

a.cs(1,7): error CS0509: 'zzz' : cannot inherit from sealed class 'int'

 

We cannot derive from any of the value types as they are explicitly sealed. This deterrence restricts you from tinkering around with the guts of the compiler unnecessarily. 

 

There is one fundamental difference between a value and a reference type. While copying in a value type, a variable value is copied by creating a separate copy of the variable in memory. In a reference type, only a reference to the object is copied and not the actual contents.

 

All value types have a constructor that is called at the time of creation of the object. Thus, when we write int i, this free public parameter-less constructor is called. This is also the default constructor. For all the simple types, this default constructor initializes the variable to zero or in the case of floating point types, to be specific, 0.0. Enums are constants and so their values are also initialized to zero. In the case of a struct, all value types are made zero and reference type are initialized to null.

 

a.cs

class zzz

{

public static void Main()

{

aaa a;

}

}

struct aaa

{

public int i;

public string j;

}

 

Compiler Warning

a.cs(10,12): warning CS0649: Field 'aaa.i' is never assigned to, and will always have its default value 0

a.cs(11,15): warning CS0649: Field 'aaa.j' is never assigned to, and will always have its default value null

 

Need we add anything more? The compiler after a very long time agrees with us to no end and repeats what we told you in the earlier paragraph. If error messages and warnings were more verbose, would you want such weighty books? We surely would be out of a job.

 

a.cs

class zzz {

int i;

public static void Main()

{

int j = new int();

zzz a = new zzz();

System.Console.WriteLine(a.i+ " " + j);

}

}

 

Compiler Warning

a.cs(3,5): warning CS0649: Field 'zzz.i' is never assigned to, and will always have its default value 0

Output

0 0

 

Whether we write int i or new int(), the effect is the same. An int object/variable  is created and the constructor is called. This initializes the variable to zero. Thus, use of new is optional for the basic types. We thank the compiler for such small mercies.

 

a.cs

class zzz

{

public static void Main()

{

int j = new int(10);

}

}

 

Compiler Error

a.cs(5,9): error CS0143: The type 'int' has no constructors defined

 

We peeked into the structure System.Int32 to locate for a constructor that accepts one parameter. But to our dismay, we never found it. The designers chose not to implement such a constructor, a decision they made. And, it is at our discretion to criticize or not on their decision. Their volition is a mystery to all of us. Is anyone out there in outer space listening?

 

Also, as every structure has a constructor with no parameters, which initializes every member to its default value, we are obviously not permitted to create our own constructor with no parameters. This would create a piquant situation where we would end up having two constructors with no parameters. Whenever the compiler gives us something free, we should be highly indebted to it and never try and repeat the same code. Duplicates are a problem wherever you go.

 

The simple data types are a little different from the structure types not only because the compiler considers them to be reserved words in the language but also they are treated in a slightly different way.

 

 

First and foremost, a simple type can be initialized by using a literal 123, e.g. int i = 123. Literals cannot be used with user-defined struct types. They can be used only with simple types and the value types. The value of a struct can only be determined at run time as the constructor initializes the members. Thus, a constant expression can be created with simple value types. Score two for the simple value types.

 

At the same level, a structure type cannot be a constant. To solve this problem, the compiler introduced static readonly fields. They have the same effect as constants. Score + point for a value field. Lastly, a value type can be converted into other value types for matching a parameter in a user defined conversion operator. This is not possible for other user defined conversion parameters involving other structure types.

 

Integral Types

 

Warning. Lots of tiresome information ahead and less of Code.

 

We have nine integral types in all. They amount of memory they require and the range of values they can handle are as follows.

 

   The sbyte type represents a signed 8-bit integers with values between -128 and 127.

 

   The byte type represents unsigned 8-bit integers with values between 0 and 255.

 

   The short type represents signed 16-bit integers with values between -32768 and 32767.

 

   The ushort type represents unsigned 16-bit integers with values between 0 and 65535.

 

   The int type represents signed 32-bit integers with values between -2147483648 and 2147483647.

 

   The uint type represents unsigned 32-bit integers with values between 0 and 4294967295.

   The long type represents signed 64-bit integers with values between -9223372036854775808 and 9223372036854775807.

 

   The ulong type represents unsigned 64-bit integers with values between 0 and 18446744073709551615.

 

   The char type represents unsigned 16-bit integers with values between 0 to 65535.

 

The set of possible values for the char type corresponds to the Unicode character set. All this is copied straight from the documentation. Fastest paragraph ever written. It took exactly 1.2 seconds to accomplish the copy and paste.

 

As a re-revision, for unary operators +,- and ~ and the binary operators the operand/s are first converted to an int, uint, long or ulong. These datatypes can fully represent the range of values. The unary operator, however, first converts the operand only to an int or long. The relational operators only deals with a bool as the result value.

 

a.cs

class zzz

{

public static void Main()

{

char j = 'A';

char i = (char)65;

char k = 65;

}

}

 

Compiler Error

a.cs(7,10): error CS0031: Constant value '65' cannot be converted to a 'char'

 

A char constant has to be written as a character literal or a number with a cast. If you do not, the above error will be your reward.

 

 

 

a.cs

class zzz

{

public static void Main()

{

char j = 'A';

char l = (char)65;

char m = '\x0041';

System.Console.WriteLine(j + " " + l + " " + m);

}

}

 

Output

A  A  A

 

As there are many ways to skin a cat, there are many ways to do the same thing in C#. Thus, we can also use the hexadecimal notation to initialize a char, provided we are using literals. Also, a cast is the perfect panacea for all our problems. Any errors resulting due to conversions can always be removed using a cast.

 

Floating Point

 

We have two different floating point types in the C# programming language, Float and its elder, smarter brother double. The computer world got together and came out with a worldwide standard on storing numbers with decimal places in memory. They have to be stored differently from a number. This standard could not go nameless and hence it was called the IEEE 754 standard. It is freely available to all and stores floats and doubles as 32 bit single precision and 64 bit double precision numbers in memory.

 

Zero denotes an absence of anything. If something is not present, then how in the first place, can we detect it? Do we have a concept of positive zero and negative zero? These are questions to be answered by philosophers and the mathematics doctors.

 

Most of the time zeroes are the same but at times, albeit rarely, they are different. If we have the time, we will give you a short exposition on zeroes. To complicate matters even further, infinity also cannot be measured. The moment you can, it stops being infinite. It is like the sound of a one hand clap. Thus we have like zeroes, positive and negative infinity. 1.0/0.0 is +ve infinity and -1.0/0.0 is negative infinity. When we divide a zero by a zero. What should we get as the answer. Obviously something that is Not a Number or NAN, which is also an invalid number.

 

Let us now understand the vastness of numbers that a floating point type can store. If the formula can be written as s * m * 2^e where s is either 1 or -1. The range of m is from o to 2 ^24 for a float and e lies form -149 to 104. For a double m lies form 0 to 2^53 and e form -1075 to 970. To put the above numbers in the right perspective, a float can represent values ranging from 1.5 + 10"45 to 3.4 + 1038 with a precision of 7 digits whereas the double data type is nearly double that of the float. It is from 5.0 + 10"324 to 1.7 + 10308 with a precision of 15-16 digits.

 

If you walked in late to the party, for binary operators, the rule is very simple. First, there is a conversion to double and then to a float for any one of the operands that is a integer type and the other, a double or float. Floating point types hate producing exceptions and thus do not throw them at all. They show their displeasure in other ways. If the answer is too small for the destination type, a +ve or a -ve zero is the final result and if it is too large, it will be +ve or -ve infinity. If the answer does not follow the IEE 754 specifications, a NAN is outputted. A NaN does not like to coexist with any other number, and thus if any operand produces a NaN, the result of the operation is a NaN.

 

C# is very flexible in the handling of floating point types in memory. The above range is only suggestive at the lower end. The compiler only enforces a bare minimum and not a maximum. Hardware architectures keep evolving and there are some, where a floating point number is stored internally using more memory than what is specified by the IEEE 754 specifications. If the compiler had to adhere to the IEEE format, it would slow down the machine even though we are asking the computer to use less memory to do the calculations. Thus, the compiler allows us even more leeway here. It lets us more than double the range of the floating point and also does not compromise on speed. More is good, less is bad!

Decimal Types

 

If you have been impressed by the floating point types, now is the time to remove the scales from your eyes. For a large number of financial and monetary computations, the floating point types are not accurate enough and introduce a rounding off error. The decimal type needs 128 bits or 16 bytes of memory to store itself. The range, it can handle all by itself, starts from 1.0 + 10"28 to approximately 7.9 + 1028 with 28-29 significant digits. Using the same formula earlier s * m * 10^e, s is 1 or -1 , m from 0 to 2 ^ 96 and e from -28 to 0. The decimal type unlike floating point does not understand signed zeros, infinities and NaN's.

 

A decimal is represented by a 96 bit integer scaled by a power of 10. If the absolute value is less than 1.0m, the number is accurate to the 28th decimal place. Great. But if the value is greater than 1, the value is exact as per the 28 or 29 digit. The problem with a float/double is its ability to represent numbers like 10/3 or .1 in an accurate fashion. These are infinite fractions and are prone to round off errors. The decimal format does not have these problems.

 

For a binary operator if any of the operands is a decimal, the other must either be a decimal or a integer and not a floating point number in any circumstance. A useful tidbit. All decimal numbers are rounded off to their nearest possible value and if two values are equally close, the one that has an even number in the least significant or last digit is used.

 

If a decimal operation produces an answer that is very small for the decimal format after rounding, unlike a floating point type, the result is zero. However, if the result is too large, an Overflow Exception is thrown. Unlike floats, the decimal has no qualms about using exceptions.

 

Now, to sum up the differences between floats and decimals. The decimal type has greater precision or accuracy than the floating point type but at the cost of a smaller range. Thus if we convert from a floating point to a decimal, we may get an overflow as the float can store higher values. Conversion from decimal to float can cause loss of precision instead. Thus, there exists no known conversion between floating point types and decimals. You can only mix them in an expressions by using an explicit cast.

 

A bool is antisocial, hating interaction with any other type. A bool cannot be converted to any other type, not even an integer. It prefers its solitude.

 

a.cs

class zzz

{

public static void Main()

{

bool i = false;

int j = (int)i;

i = (bool)1;

}

}

 

Compiler Error

a.cs(6,10): error CS0030: Cannot convert type 'bool' to 'int'

a.cs(7,6): error CS0030: Cannot convert type 'int' to 'bool'

 

When in doubt, use a cast. It does not work with bool. No explicit conversions exist from a bool to any other type. Read our lips. That's it.

 

Boxing and Unboxing

 

This is a central concept that unifies the type system of C#, which consists of value and reference types. Boxing is a way by means of which a value type becomes a reference type and unboxing does the reverse, i.e. it converts a reference type into a value type. It binds these two types. The resulting reference type is an object naturally.

 

Boxing, thus, presents a unified view of the type system where everyone is equal before the law as an object type. A value type can also implement an interface, and boxing not only converts a value type to object but also to any interface type implemented by the value type. In other words, boxing creates an object and copies the values in the value type to the ones in the object.

Conceptually, we can view a boxing conversion as follows. Assuming we had a value type int and we want to box this value or convert it into an object. We would internally declare a class as follows.

 

a.cs

class int_box

{

int x;

public int_box(int t)

{

x = t;

}

}

 

int i = 123;

object b = i;

 

This becomes

 

int i = 123;

object b = new int_box(i);

 

When we write the above lines, internally the compiler can create a class that has any name. We have chosen int_box as the name of the class. The above is conceptual, it may not happen the way we've explained it. First, the compiler creates a new object that looks like the class int_box. This has a constructor that accepts one parameter that simply initializes the variable x, representing the value type int. Thus, we have now converted an int into an object. Replace int with the data type you want to promote it to, an Object and the above explanation then will hold water.

 

a.cs

class zzz

{

public static void Main()

{

int i = 123;

object b = i;

if ( b is int)

System.Console.WriteLine("true");

if ( b is object)

System.Console.WriteLine("true1");

if ( b is long)

System.Console.WriteLine("true2");

}

}

 

Output

true

true1

 

The object b has a dual role to perform.  Thus, the above classes are not created and in spite of b being an object it is now also an int.

 

a.cs

class zzz

{

public static void Main()

{

object b = ‘a’ ;

if ( b is int)

System.Console.WriteLine("true");

if ( b is object)

System.Console.WriteLine("true1");

}

}

 

Output

true1

 

By default, an object is first only an object and nothing else. In the previous program, we promoted int i to become a reference type, hence the object b could either be treated as an object or a value type int at the same time.

 

a.cs

class zzz

{

public static void Main()

{

int b = 11;

if ( b is int)

System.Console.WriteLine("true");

if ( b is object)

System.Console.WriteLine("true1");

}

}

 

Output

true

true1

 

Also, everyone is derived from object. The earlier example demonstrates this point.

 

a.cs

class zzz {

public static void Main()

{

xxx a = new xxx(1);

object b = a;

a.x = 2;

System.Console.WriteLine(((xxx)b).x);

yyy c = new yyy(1);

object d = c;

c.x = 2;

System.Console.WriteLine(((yyy)d).x);

}

}

struct xxx

{

public int x;

public xxx(int i)

{

x = i;

}

}

class yyy {

public int x;

public yyy(int i)

{

x = i;

}

}

Output

1

2

 

This is the single-most important distinction between a class and a struct or between value types and reference types. When we equate object b to value type a, we are creating a new copy of the int in memory and initializing this new memory to the value of the individual members of the struct. Thus, at the end of the statement, we now have two identical structures in different areas of memory with no linkages between the two. Changing the value of x from b does not change the value of x in a as they are independent of each other.

 

However, with reference types, things change a lot. We are not copying the object c but storing a number in d that signifies where this object starts in memory. Hence, there is only one object c in memory which can be accessed either using c or d. Thus changing in one will reflect the change in the other.

 

Unboxing is reversing the above process, that is, converting an object type into value type. A check has to be performed first whether the object can be converted to the value type. If the check results true then the copy work into the value type should be initiated. Referring to our earlier example, unboxing would read as.

 

object b = net int_box(1);

int I = ((int_box)b).x;

 

a.cs

class zzz

{

public static void Main()

{

long f = 1;

object b = f;

int i = (int)b;

System.Console.WriteLine(i);

}

}

 

Compiler Error

Exception occurred: System.InvalidCastException: An exception of type System.InvalidCastException was thrown.

   at zzz.Main()

 

A type cannot be unboxed to a new type if it has been boxed earlier as this will throw an exception at run time. However, it can be converted to the original. The language does a type check for boxing and unboxing as the type must be the same. A run time type check is carried out for unboxing operations. So think twice before you unbox.

 

In the above instance, we started with a long. We boxed it to an object b that is not only an object but also a long. We then tried to unbox this object into an int and not a long which causes problems.

 

a.cs

class zzz

{

public static void Main()

{

long f = 1;

object b = f;

long  i = b;

}

}

 

Compiler Error

a.cs(7,11): error CS0029: Cannot implicitly convert type 'object' to 'long'

 

We can convert an int into an object without the compiler screaming errors at us. We cannot do the reverse, that is convert an object b, into a long even though b also stands for a long. Tough luck.

 

There is an implicit conversion available from the derived class to the base class. We do not have to create one as it is already present. A lot more on implicit conversion is explained in one of the coming chapters.

 

 

a.cs

class yyy

{

}

class xxx : yyy

{

public static implicit operator yyy ( xxx a)

{

}

}

 

Compiler Error

a.cs(6,15): error CS0553: 'xxx.implicit operator yyy(xxx)': user-defined conversion to/from base class

 

As such, a conversion is already available so we cannot create one ourselves.

 

a.cs

class yyy

{

public static implicit operator xxx ( yyy a)

{

return new xxx();

}

}

class xxx : yyy

{

}

 

Compiler Error

a.cs(3,15): error CS0554: 'yyy.implicit operator xxx(yyy)': user-defined conversion to/from derived class

 

It shows the same error and it is proof that we ignored the earlier error message. We cannot convert from or to a base/derived class ever as it is handled internally for us.

 

a.cs

class zzz

{

public static void Main()

{

yyy a = new yyy();

xxx b = new xxx();

a = b;

b = (xxx)a;

}

}

class yyy

{

}

class xxx : yyy

{

}

 

There exists an explicit cast to convert a base class to a derived class and an implicit cast to convert a derived class to a base class.

 

Definite Assignment

 

A variable is definitely assigned if the compiler can prove that either the variable has been automatically assigned or it has been the target of at least one assignment. The rules for definite assignment are as follows.

 

   Every variable that has been initially assigned is also definitely assigned.

 

   Before we reach a variable by any possible path, a simple assignment statement is encountered where this variable is the left operand.

 

   We call a function where the variable is being passed as a left operand

 

   Finally, a local variable includes a variable initializer. 

 

For a structure, the state of a variable is tracked individually as well as collectively. Thus, for a structure to be definitely assigned, we need all the individual members to be definitely assigned.

 

There are a large number of reasons for the variable to be definitely assigned.

 

Whenever a variable is accessed for its value, it must have been definitely assigned otherwise an undefined variable error will take place. An unassigned variable cannot be accessed for its value. A variable can be initialized in three ways, by being on the left of an assignment, being passed as an out parameter or finally appearing as a left operand in a structure access.

 

We told you earlier that a ref parameter can be used only in a function invocation if it has been initialized earlier outside the function. Thus, ref parameters are always definitely assigned. Out parameters for a function must be initialized before the function terminates. Thus, an out parameter will always be considered definitely assigned when we exit the function.

 

a.cs

class zzz

{

public static void Main()

{

}

public void abc()

{

int i,j;

try

{

i = 10;

}

catch

{

System.Console.WriteLine(i + " " + j);

}

}

}

 

Compiler Error

a.cs(15,26): error CS0165: Use of unassigned local variable 'i'

a.cs(15,36): error CS0165: Use of unassigned local variable 'j'

 

In spite of explicitly initializing the variable i in the try block, for the catch the variable i is not initialized. The compiler for some reason believes that we can enter the catch block without executing the assignment statement of the try block. Thus, in the catch where we are using the variables i and j, the compiler believes that they have not been initialized at all and thus generates the error.

 

a.cs

class zzz

{

public static void Main()

{

}

public void abc()

{

int i,j;

try

{

i = 10;

}

catch

{

}

finally

{

System.Console.WriteLine(i + " " + j);

}

}

}

 

Compiler Error

a.cs(18,26): error CS0165: Use of unassigned local variable 'i'

a.cs(18,36): error CS0165: Use of unassigned local variable 'j'

 

Ditto for the finally which always is called at the end of the try. Hence, before you enter a try-catch-finally block, please initialize all the variables. The above conclusions, we repeat have been reached by a process known as static flow analysis.

 

 

 

a.cs

class zzz

{

public static void Main()

{

}

public void abc()

{

int x=1,y=2;int i;

if ( x >= 2 && (i=y) != 1)

System.Console.WriteLine("one " + i);

else

System.Console.WriteLine("two " + i);

System.Console.WriteLine("three " + i);

}

}

 

Compiler Error

a.cs(12,35): error CS0165: Use of unassigned local variable 'i'

 

a.cs

class zzz

{

public static void Main()

{

}

public void abc()

{

int x=1,y=2;int i;

if ( x >= 2 || (i=y) != 1)

System.Console.WriteLine("one " + i);

else

System.Console.WriteLine("two " + i);

System.Console.WriteLine("three " + i);

}

}

 

Compiler Error

a.cs(10,35): error CS0165: Use of unassigned local variable 'i'

 

Two programs, exactly the same, with the only change of && to ||. We are in either case initializing variable i to variable y in the if statement. Both variables  x and y have been definitely assigned. What we realize form the above answers is that for a && the if statement does not complain and for a || the else does not complain. But in both cases after we leave the if statement, the variable i is not initialized. We understand after we leave the if because the && and || are short circuit operators and the second condition may not be called depending on the answer obtained for the first condition.

 

If we change the || or && to a single | or &, the error disappears as these are non short circuit operators and they will always be called. The values of the variables do not seem to make any difference on the final answer. However, if you use a constant like true or false then the compiler knows whether the first condition will always be true or false as the case may be and then will try and optimize everything. There is no reason why it should behave differently for a && and a ||. Beyond our comprehension! The documentation says that for a &&, it will first execute the assignment and then the other stuff like the embedded statements following the if. For a || Reverse for the ||. Go figure it out yourself. We gave up a long time back!

 

There are, to be precise, six cases of variables that are initially assigned. These are static variables, instance variables or public variables in classes, Instance variables of initially assigned structures, arrays, value and reference parameters. Initially assigned means that the above variables are guaranteed to have a value. For example, instance variables are always assigned the default value etc. We have three cases of variables which are unassigned and whose value cannot be used unless we initialize them first. These are local variables created in a function, Out parameters to a function including the this variable of constructors in a struct and instance variables of initially unassigned structures.  

 

A variable reference is a location in memory that stores the value of a variable such that we can read or write/change the value of the variable. There are the three places where a variable reference must be specified. Nothing else will suffice. These places are the left hand side or left operand of an assignment, any parameter to a function or constructor denoted as ref or out.

 

 

Constants

 

A constant cannot consist of any data type. They are restricted to the following types only. These are sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, string, enum type, or the null type.

 

We are also restricted in the constructs that a constant expression can use. These are literals, any sub expressions, casts, the usual gang of suspects in unary operators + - ! and -, the binary operators +, -, *, /, %, <<, >>, &, |, ^, &&, ||, ==, !=, <, >, <=, and => and let us not forget the single ternary operator ?:. Lest we forget, we can also refer to the other const members that make up a struct or a class.

 

Remember the value of a const is evaluated at compile time only. What happens for non-const expressions at run time, a similar thing happens for a const at compile time. The only difference is that run time checks always generate exceptions whereas compile time checks results in an error from the compiler. The default mode is checked, hence, the compiler entraps any overflows in value. We are allowed to use const in constant declarations, enums, case statements, goto case, dimension lengths while creating an array and finally with an attribute. We also have a free implicit constant conversion that converts an int to a sbyte, byte, short, ushort, uint, or ulong, assuming that the value fits in the destination type.

 

Casting

 

When we write a statement as  (x)-y, the compiler is confused as it comes across an ambiguity. What were are intentions?? Are we trying to subtract x from y, or casting -y to a type x?? To clarify these doubts, the language follows some more rules of casting which determines whether a token in a parenthesis is to be considered a cast or not.

 

   If the token follows the rules for a type and not an expression, it is a cast.

 

   The second rule adds on to the first and states that immediately after we end the cast we come across a ~, !, (, identifier, literals or a keyword excluding is, then the expression is evaluated first and then casted.

 

The ramifications of the new rules will be enumerated further. Thus, the compiler is very strict about treating something as a cast. If it does not follow the above rules, it cannot be a cast expression.

 

Thus, the general rule is anything in parenthesis is treated as an expression. If it does not evaluate to an expression, then and only then a cast is considered. Thus, a cast has the lowest priority in the eyes of a compiler. In other words, the compiler does not like casts. The rules have to be syntactically correct. The compiler as usual does not look into the meaning of what you are trying to say. Remember they are intelligent, but not in the way we think. You could do the dumbest thing in the world, but as long as you have followed the syntax rules, the compiler gives you the green light.

 

If a and b are two identifiers, then a.y is the right grammar/way to write a type. This is so in spite of the fact that a.y cannot represent a type at all. a may be the name of a variable and not object for all that the compiler cares. If you have understood the earlier rules which we have not, then  (a)b, (a)(b) and (a)(-b) are all casts but (a) -b is not, even if a identifies a type. Keywords, however, come first as they cannot be present in an expression all by itself. Thus if y happened to be a byte for example, the above four possibilities would be casts only. As a repetition, a cast is the last refuge of a scoundrel.

 

The Other Odds and Ends

 

a.cs

class zzz

{

float f = 1.1E400;  

}

 

Compiler Error

a.cs(3,11): error CS0594: Floating-point constant is outside the range of type 'double'

We have limits on everything in life. A float may be a very large number, but it yet has a upper limit. We crossed that limit in the above program.

 

a.cs

public class zzz

{

private static byte i = 0;

public static void Main()

{

if ( i == 256)

i=0;

}

}

 

Compiler Warning

a.cs(6,6): warning CS0652: Comparison to integral constant is useless; the constant is outside the range of type 'byte'

 

The compiler pounces at anything that he can find wrong at the time of compilation. Here it finds us comparing a byte to a value that the byte can never aspire to. A value larger than what a byte in its sweet dreams can ever store 256 larger than the range 1 to 255. Nothing earth shaking as the if will always be false.

 

a.cs

public class zzz

{

public void abc()

{

string s = "\m";

}

}

 

Compiler Error

a.cs(5,12): error CS1009: Unrecognized escape sequence

 

In a string anything that begins with a \ character is called an escape sequence. There are only a finite number of such escape sequences defined. \m is not one of them and hence an error. Think twice before using the \ character.

a.cs

public class zzz

{

public void abc()

{

char s = '';

}

}

 

Compiler Error

a.cs(5,10): error CS1011: Empty character literal

 

A char variable must be initialized to some defined character literal or value and not to a empty one. Nobody likes emptiness.

 

a.cs

public class zzz

{

public void abc()

{

char s = 'xx';

}

}

 

Compiler Error

a.cs(5,10): error CS1012: Too many characters in character literal

 

A character literal is made up of a single character in a set of single inverted commas. Internally it takes up two bytes to be stored. We get an error as we have placed two characters inside the inverted commas.

 

a.cs

public class zzz

{

long i = 0xFFFFFFFFFFFFFFFFFL ;

}

 

Compiler Error

a.cs(3,10): error CS1021: Integral constant is too large

 

Whenever we exceed the range of values a variable can store, we get an error like above.

 

a.cs

class zzz

{

int a=1000000000000000000000 ;

}

 

Compiler Error

a.cs(3,7): error CS1021: Integral constant is too large

 

Lots of money is bad and a larger value than what a type can hold is even worse!