Introduction
In part II of this multipart tutorial, I describe the keywords that can loosely be categorized as dealing with object type, operators and type conversion.
Why Is This Useful To You?
My interest in this series of articles is to provide a concise repository for the entire collection of keywords in C#. I plan to use this myself as a reference, and hopefully you will find it useful too. In Part I[^], I provided an overview of all the keywords and described in detail, with some code examples, the keywords that can be considered modifiers to entities such as methods, parameters, etc.
In the "old" days, languages used to be taught differently: start with the keywords, then move into the syntax, and then start writing some basic (as in simple!) programs. This approach, while it doesn't satisfy the "immediate gratification" crowd is still a valid approach when trying to gain a well rounded understanding of a programming language.
Why Is This Better Than Microsoft's Help On Keywords?
The primary answer to that question is not so much that what I have to say is better, but that in some cases, I provide supplementary understanding that the help topics don't provide. Also, personally, I get tired of bouncing around from one link to another. Sometimes it's nice to see everything laid out in a contiguous way. Also, I find that the more ways a concept can be described, the better I'll understand it, and there's nothing better to test one's comprehension than by trying to explain it to someone else!
What Have I Learned?
In writing Part II, I've learned that unary operators appear (to this author) pretty much useless because operators are required to be static members. Also, binary operators are limiting (this is true for unary operators as well) because they cannot return references to objects. So much for implementing streams in C# or other features found in STL. It seems that without the ability to operate on information in the enclosing class, and without the ability to return references, C# will never support the nice capabilities of templates and STL that we've grown to love using C++ (of course, I could be wrong!).
I also learned a lot about the inner "logic" of C#, especially regarding the rules for implicit and explicit casts.
The Keywords
Following is a table of (almost all) the keywords in C#.
abstract | event | new | struct |
as | explicit | null | switch |
base | extern | object | this |
bool | false | operator | throw |
break | finally | out | true |
byte | fixed | override | try |
case | float | params | typeof |
catch | for | private | uint |
char | foreach | protected | ulong |
checked | goto | public | unchecked |
class | if | readonly | unsafe |
const | implicit | ref | ushort |
continue | in | return | using |
decimal | int | sbyte | virtual |
default | interface | sealed | volatile |
delegate | internal | short | void |
do | is | sizeof | while |
double | lock | stackalloc | |
else | long | static | |
enum | namespace | string | |
Keywords dealing with object type and type conversions:
These are the keywords that will be discussed in this article:
as
explicit
implicit
is
operator
sizeof
typeof
The "as" Keyword
The as
keyword attempts to convert one object to another, similar to a cast. If the conversion fails, a null
is returned instead of an exception being thrown. For example:
object n=123;
object obj="abc";
string s1=n as string;
string s2=obj as string;
Console.WriteLine("s1="+s1);
Console.WriteLine("s2="+s2);
results in:
s1=
s2=abc
Note that in some cases, the compiler will generate an error because it is attempting to do the cast at compile time.
int i=123;
string s1=i as string;
results in a compiler error: "Cannot convert type 'int' to 'string' via a built-in conversion."
To illustrate the difference between a cast and using the as
operator, note that the following code:
string s3=(string)n;
generates an exception, whereas, when using the as
operator, the string s1
was merely set to null
.
Note that the as
keyword contains an implicit "is" test, using the keyword described next.
The "is" Keyword
The is
operator tests whether an object is of a particular type. For example:
object n1=123;
bool b1=n1 is int;
bool b2=n1 is string;
Console.WriteLine("n1 is int ? "+b1.ToString());
Console.WriteLine("n1 is string ? "+b2.ToString());
Results in:
n1 is int ? true
n1 is string ? false
With the as
keyword, the compiler would generate a compile time error if it could resolve the object cast at compile time. Similarly, the compiler will generate a warning if the is
operator evaluates to true
or false
at compile time--meaning that the expression is always either true or false.
The "operator" Keyword
This keyword declares an operator on a class or structure. An operator does two things. The first is it performs an operation on one or two parameters, including a conversion from the parameter type to the resulting type. The second thing an operator does is to perform a conversion from one type to another. This capability is used in conjunction with the explicit
and implicit
keywords, and will be discussed in the next two sections. In this section, we'll look at operators that perform an operation.
Operators are always public
and they are always static
. An operator is therefore always specified as:
public static ...
Unary Operators
A unary operator takes one operand and manipulates it. The operand must be of the same type as the containing class
or struct
. Because the operator function is declared as static, it cannot reference anything except the members in its operand. This differs from C++, and in my opinion, makes unary operators basically useless except in highly specialized cases where it actually means something to "add" or "subtract" the members of an operand.
For example:
public class PointEx
{
protected Point p;
public PointEx() {p=new Point(0, 0);}
public PointEx(int x, int y) {p=new Point(x, y);}
public PointEx(Point p) {this.p=p;}
public static int operator +(PointEx p)
{
return p.p.X+p.p.Y;
}
}
can be used syntactically as:
PointEx p=new PointEx(1, 2);
int n=+p;
which results in n=3. This seems fairly pointless. However, overloading the "negative", "not", "exclusive-or", "increment" and "decrement" unary operators may be useful in certain instances.
Binary Operators
Binary operators perform an operation on two operands. At least one of the operands must be of the the enclosing struct
or the class
. This mechanism prevents you from redefining built in operators, for example, public static int operator +(int a, int b) {return a + b -1;}
is not legal. An example of a binary operator is:
public class PointEx
{
protected Point p;
public PointEx() {p=new Point(0, 0);}
public PointEx(int x, int y) {p=new Point(x, y);}
public PointEx(Point p) {this.p=p;}
public static PointEx operator +(PointEx p, int scalar)
{
return new PointEx(p.p.X+scalar, p.p.Y+scalar);
}
}
PointEx p=new PointEx(1, 2);
p+=5;
The Cast Operator
The cast operator is, in my opinion, another fairly useless item because, unlike C++, it cannot return a reference. This means that you cannot cast perform a cast on an "l-value". For example:
public class PointEx
{
protected Point p;
public static implicit operator Point(PointEx p)
{
return p.p;
}
public static PointEx operator +(PointEx p, int xlat)
{
((Point)p).X+=xlat;
((Point)p).Y+=xlat;
return p;
}
}
In the operator +
method, the ((Point)p.).X
cast is illegal because the operator
Point
cast is not, and can not, as the language is currently defined, return a reference.
However, with the above code, we could of course write:
Point pp=p;
and the compiler will dutifully and automatically invoke the cast operator which returns the Point
member of PointEx
.
The "implicit" Keyword
The above example uses the implicit
keyword in the definition of the operator. This defines it as a cast, and one that will occur automatically without the programmer knowing it.
The "explicit" Keyword
The explicit
keyword also defines a cast operation but requires that the programmer explicitly selects the cast that is to be performed. If the cast operation has been defined, the compiler will dutifully generate the code to invoke the cast. For example, (modifying the above class):
public static explicit operator Point(PointEx p)
{
return p.p;
}
Now, the implicit cast we used before generates a compiler error:
Point pp=p;
Point pp2=(Point)p;
What are the rules in determining whether a cast should be defined as implicit or explicit?
Microsoft has this to say on the subject:
By eliminating unnecessary casts, implicit conversions can improve source code readability. However, because implicit conversions can occur without the programmer's specifying them, care must be taken to prevent unpleasant surprises. In general, implicit conversion operators should never throw exceptions and never lose information so that they can be used safely without the programmer's awareness. If a conversion operator cannot meet those criteria, it should be marked explicit.
The "sizeof" Keyword
The sizeof
keyword can only be used in "unsafe" mode. It returns the size of the object. For example:
int i=sizeof(short)
results in i
=2.
Note: I am not sure why this keyword can only be used in "unsafe" mode. I assume that the reason this is an "unsafe" keyword is that the underlying "generic" Instruction Language (IL) requires compiler and platform specific information to resolve the size of an object--whether a simple type like an integer or, a complex type like an object with multiple derivations or interfaces.
The "typeof" Keyword
This keyword returns the type of an object (not an instance). To return the type of an instance, use the GetType
member. For example:
double d=0;
Type t1=d.GetType();
Type t2=typeof(double);
These two statements are equivalent, except that GetType
is applied to the instance of an object, whereas typeof
is applied to the definition of an object. The value of obtaining a Type
at run time is in the richness of this object--it includes information regarding access (private
or public
), and whether the object is a class, an interface or an ANSI class (just to name a couple). The Type
object also has methods to extract the type's members and methods. This can be useful in conjunction with functions in the System.Reflection
namespace.
Conclusion
I hope this series of articles helps anyone interested in understanding the capabilities of C#!