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!