Tải bản đầy đủ (.pdf) (80 trang)

Thinking in C# phần 7 pptx

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (605.55 KB, 80 trang )


494 Thinking in C# www.ThinkingIn.NET
Random access with Seek
The Stream base class contains a method called Seek( ) that can be used to jump
between records and data sections of known size (or sizes that can be computed
by reading header data in the stream). The records don’t have to be the same size;
you just have to be able to determine how big they are and where they are placed
in the file. The Seek() method takes a long (implying a maximum file size of 8
exabytes, which will hopefully suffice for a few years) and a value from the
SeekOrigin enumeration which can be Begin, Current, or End. The
SeekOrigin value specifies the point from which the seek jumps.
Although Seek( ) is defined in Stream, not all Streams support it (for instance,
one can’t “jump around” a network stream). The CanSeek bool property
specifies whether the stream supports Seek( ) and the related Length( ) and
SetLength( ) mehods, as well as the Position( ) method which returns the
current position in the Stream. If CanSeek is false and one of these methods is
called, it will throw a NotSupportedException. This is poor design. Support
for random access is based on type, not state, and should be specified in an
interface (say, ISeekable) that is implemented by the appropriate subtypes of
Stream.
If you use SeekOrigin.End, you should use a negative number for the offset;
performing a Seek( ) beyond the end of the stream moves to the end of the file
(i.e., ReadByte( ) will return a -1, etc.).
This example shows the basic use of Stream.Seek( ):
//:c12:FibSeek.cs
using System;
using System.IO;

class FibSeek {
Stream src;


FibSeek(Stream src){
this.src = src;
}

void DoSeek(SeekOrigin so){
if (so == SeekOrigin.End) {
src.Seek(-10, so);
} else {

Chapter 12: I/O in C# 495
src.Seek(10, so);
}
int i = src.ReadByte();
Console.WriteLine(
"10 bytes from {0} is : {1}", so, (char) i);
}

public static void Main(string[] args){
foreach(string fName in args){
FileStream f = null;
try {
f = new FileStream(fName, FileMode.Open);
FibSeek fs = new FibSeek(f);
fs.DoSeek(SeekOrigin.Begin);
fs.DoSeek(SeekOrigin.End);
f.Seek(12, SeekOrigin.Begin);
fs.DoSeek(SeekOrigin.Current);
} catch (Exception ex) {
Console.WriteLine(ex);
} finally {

f.Close();
}
}
}
}///:~
Standard I/O
The term standard I/O refers to the Unix concept (which is reproduced in some
form in Windows and many other operating systems) of a single stream of
information that is used by a program. All the program’s input can come from
standard input, all its output can go to standard output, and all of its error
messages can be sent to standard error. The value of standard I/O is that
programs can easily be chained together and one program’s standard output can
become the standard input for another program. More than just a convenience,
this is a powerful architectural pattern called Pipes and Filters; although this
architecture was not very common in the 1990s, it’s a very powerful one, as
anyone who’s witnessed a UNIX guru can testify.

496 Thinking in C# www.MindView.net
Reading from standard input
Following the standard I/O model, the Console class exposes three static
properties: Out, Error, and In. In Chapter 11 we sent some error messages to
Console.Error. Out and Error are TextWriters, while In is a TextReader.
Typically, you either want to read console input as either a character or a
complete line at a time. Here’s an example that simply echoes each line that you
type in:
//:c12:EchoIn.cs
//How to read from standard input.
using System;

public class EchoIn {

public static void Main(){
string s;
while ((s = Console.In.ReadLine()).Length != 0)
Console.WriteLine(s);
// An empty line terminates the program
}
} ///:~

Redirecting standard I/O
The Console class allows you to redirect the standard input, output, and error
I/O streams using simple static method calls:
SetIn(TextReader)
SetOut(TextWriter)
SetError(TextWriter)
(There is no obvious reason why these methods are used rather than allowing the
Properties to be set directly.)
Redirecting output is especially useful if you suddenly start creating a large
amount of output on your screen and it’s scrolling past faster than you can read
it. Redirecting input is valuable for a command-line program in which you want
to test a particular user-input sequence repeatedly. Here’s a simple example that
shows the use of these methods:
//:c12:Redirecting.cs
// Demonstrates standard I/O redirection.
using System;

Chapter 12: I/O in C# 497
using System.IO;

public class Redirecting {
public static void Main(){

StreamReader sr = new StreamReader(
new BufferedStream(
new FileStream(
"Redirecting.cs", FileMode.Open)));
StreamWriter sw = new StreamWriter(
new BufferedStream(
new FileStream(
"redirect.dat", FileMode.Create)));
Console.SetIn(sr);
Console.SetOut(sw);
Console.SetError(sw);

String s;
while ((s = Console.In.ReadLine()) != null)
Console.Out.WriteLine(s);
Console.Out.Close(); // Remember this!
}
} ///:~

This program attaches standard input to a file, and redirects standard output and
standard error to another file.
Debugging and Tracing
We briefly discussed the Debug and Trace classes of the System.Diagnostics
namespace in chapter 6. These classes are enabled by conditionally defining the
values DEBUG and TRACE either at the command-line or in code. These
classes write their output to a set of TraceListener classes. The default
TraceListener of the Debug class interacts with the active debugger, that of
the Trace class sends data to the console. Customizing both is easy; the
TextWriterTestListener decorates any TextWriter with TestListener
capabilities. Additionally, EventLogTraceListener ; sending data to the

console or the system’s event logs takes just a few lines of code:
//:c12:DebugAndTrace.cs
//Demonstates Debug and Trace classes
#define DEBUG
#define TRACE


498 Thinking in C# www.ThinkingIn.NET
using System;
using System.Diagnostics;

class DebugAndTrace {
public static void Main(){
TextWriterTraceListener conWriter =
new TextWriterTraceListener(Console.Out);
Debug.Listeners.Add(conWriter);
Debug.WriteLine("Debug to stdout");

EventLogTraceListener logWriter =
new EventLogTraceListener("DebugTraceProg");
Trace.Listeners.Add(logWriter);
Debug.Listeners.Add(logWriter);

Trace.WriteLine("Traced");
Debug.WriteLine("Debug trace");
logWriter.Close();
}
}///:~

When run, both Debug and Trace are written to the console. In addition, an

EventLogTraceListener object whose Source property is set to
“DebugTraceLog.” This value is used to show in the system’s event logs the source
of trace information:

Chapter 12: I/O in C# 499

Figure 12-2: Using the system Event Viewer to see program output
If you wish to create your own event log, that’s easy, too:
EventLog log = new EventLog("MySecond.log");
log.Source = "DebugAndTraceProgram";
EventLogTraceListener logWriter =
new EventLogTraceListener(log);
I think this section could be expanded a bit.
Regular expressions
Regular expressions are a powerful pattern-matching tool for interpreting and
manipulating strings. Although regular expressions are not necessarily related to
input and output, it is probably their most common application, so we’ll discuss
them here.
Regular expressions have a long history in the field of computer science but
continue to be expanded and improved, which gives rise to an intimidating set of
capabilities and alternate routes to a given end. The regular expressions in the
.NET Framework are Perl 5 compatible but include additional features such as
right-to-left matching and do not require a separate compilation step.
The fundamental responsibility of the System.Text.RegularExpressions
Regex class is to match a given pattern with a given target string. The pattern is
described in a terse notation that combines literal text that must appear in the

500 Thinking in C# www.MindView.net
target with meta-text that specifies both acceptable variations in text and desired
manipulations such as variable assignment or text replacement.

This sample prints out the file names and lines that match a regular expression
typed in the command line:
//:c12:TGrep.cs
//Demonstrate basic regex matching against files
using System;
using System.IO;
using System.Text.RegularExpressions;

class TGrep {
public static void Main(string[] args){
TGrep tg = new TGrep(args[0]);
tg.ApplyToFiles(args[1]);
}
Regex re;

TGrep(string pattern){
re = new Regex(pattern);
}

void ApplyToFiles(string fPattern){
string[] fNames =
Directory.GetFiles(".", fPattern);
foreach (string fName in fNames ) {
StreamReader sr = null;
try {
sr = new StreamReader(
new BufferedStream(
new FileStream(
fName, FileMode.Open)));
string line = "";

int lCount = 0;
while ((line = sr.ReadLine()) != null) {
lCount++;
if (re.IsMatch(line)) {
Console.WriteLine(
"{0} {1}: {2}", fName, lCount, line);
}
}

Chapter 12: I/O in C# 501
} finally {
sr.Close();
}
}
}
}///:~

The Main( ) method passes the first command-line argument to the TGrep( )
constructor, which in turn passes it to the Regex( ) constructor. The second
argument is then passed as the argument to the ApplyToFiles( ) method.
ApplyToFiles( ) uses IO techniques we’ve discussed previously to read a series
of files line-by-line and incrementing the variable lCount to let us know what
line number works. Each line is passed to the Regex.IsMatch( ) method, and if
that method returns true, the filename, line number, and contents of the line are
printed to the screen.
You might guess that “tgrep using tgrep.cs” would print lines 3, 4, and 5 of
tgrep.cs, but you might not expect that “tgrep [0-9] tgrep.cs” would print every
line that contains a number, or that “tgrep [\s]f[\w]*[\s]*= *.cs” would print
every line that assigns a value to a variable that begins with a lowercase “f”. Like
SQL in ADO.NET, the regular expression notation is a separate language quite

unlike C#, and Thinking in Regular Expressions would be quite a different book
than this one.
In addition to simply determining if a match exists, Regex can actually return
the value of the matches, as this program demonstrates:
//:c12:GrepMatches.cs
using System;
using System.IO;
using System.Text.RegularExpressions;

class GrepMatches {
public static void Main(string[] args){
GrepMatches tg = new GrepMatches(args[0]);
string target = args[1];
tg.ApplyToFiles(target);
}
Regex re;

GrepMatches(string pattern){
re = new Regex(pattern);

502 Thinking in C# www.ThinkingIn.NET
}

void ApplyToFiles(string fPattern){
string[] fNames = Directory.GetFiles(
".", fPattern);
foreach (string fName in fNames ) {
StreamReader sr = null;
try {
sr = new StreamReader(

new BufferedStream(
new FileStream(fName, FileMode.Open)));
string line = "";
int lCount = 0;
while ((line = sr.ReadLine()) != null) {
lCount++;
if (re.IsMatch(line)) {
Console.WriteLine(
"{0} {1}: {2}", fName, lCount, line);
ShowMatches(re.Matches(line));
}
}
} finally {
sr.Close();
}
}
}

private void ShowMatches(MatchCollection mc){
for (int i = 0; i < mc.Count; i++) {
Console.WriteLine(
"Match[{0}] = {1}", i, mc[i]);
}
}
}///:~

Regex.Matches( ) returns a MatchCollection which naturally contains
Match objects. This sample program can be helpful in debugging the
development of a regular expression, which for most of us requires a considerable
amount of trial and error!


Chapter 12: I/O in C# 503
The static method Regex.Replace() can make complex transformations
surprisingly straightforward. This sample makes pattern substitutions in a text
file:
//:c12:TSed.cs
using System;
using System.IO;
using System.Text.RegularExpressions;

class TSed {
public static void Main(string[] args){
TSed tg = new TSed(args[0], args[1]);
string target = args[2];
tg.ApplyToFiles(target);
}

string pattern;
string rep;

TSed(string pattern, string rep){
this.pattern = pattern;
this.rep = rep;
}

void ApplyToFiles(string fPattern){
string[] fNames =
Directory.GetFiles(".", fPattern);
foreach (string fName in fNames ) {
StreamReader sr = null;

try {
sr = new StreamReader(
new BufferedStream(
new FileStream(fName, FileMode.Open)));
string line = "";
int lCount = 0;
while ((line = sr.ReadLine()) != null) {
string nLine =
Regex.Replace(line, pattern, rep);
Console.WriteLine(nLine);
}
} finally {

504 Thinking in C# www.MindView.net
sr.Close();
}
}
}
}///:~

Like the previous samples, this one works with command-line arguments, but
this time, instead of instantiating a Regex for pattern-matching, the first two
command-line arguments are just stored as strings, which are later passed to the
Regex.Replace( ) method. If the pattern matches, the replacement pattern is
inserted into the string, if not, the line is untouched. Whether touched or not, the
line is written to the console; this makes this program a “tiny” version of UNIX’s
sed command and is very convenient.
Checking capitalization style
In this section we’ll look at a complete example of the use of C# IO which also
uses regular expression. This project is directly useful because it performs a style

check to make sure that your capitalization conforms to the C# style. It opens
each .cs file in the current directory and extracts all the class names and
identifiers, then shows you if any of them don’t meet the C# style. You can then
use the TSed sample above to automatically replace them.
The program uses two regular expressions that match words that precede a block
and which begin with a lowercase letter. One Regex matches block-oriented
identifiers (such as class, interface, property, and namespace names) and the
other catches method declarations. Doing this in a single Regex is one of the
exercises at the end of the chapter.
//:c12:CapStyle.cs
//Scans all .cs files for properly capitalized
//method and classnames
using System;
using System.IO;
using System.Text.RegularExpressions;

public class CapStyle {
public static void Main(){
string[] fNames =
Directory.GetFiles(".","*.cs");
foreach(string fName in fNames){
CapStyle cs = null;
try {

Chapter 12: I/O in C# 505
cs = new CapStyle(fName);
cs.Check();
} finally {
cs.Close();
}

}
}

string[] keyWords= new string[]{
"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", "try", "catch",
"finally", "using", "else", "switch", "public",
"static", "void", "foreach", "if", "while",
"bool", "byte", "for", "get", "set"
};

StreamReader fStream;

Regex blockPrefix;
Regex methodDef;


CapStyle(string fName){
fStream = new StreamReader(
new BufferedStream(
new FileStream(fName, FileMode.Open)));
/*

506 Thinking in C# www.ThinkingIn.NET
matches just-before-bracket identifier
starting with lowercase
*/
blockPrefix =
new Regex(@"[\s](?<id>[a-z][\w]*)[\s]*{");

/*
matches just-before-bracket with argument list
and identifier starting with lowerCase
*/
methodDef =
new Regex(
@"[\s](?<id>[a-z][\w]*)\s*\((.*)\)[\s]*{");

Console.WriteLine(
"Checking file: " + fName);
}

void Close(){
fStream.Close();
}


void Check(){
string line = "";
int lCount = 0;
while ((line = fStream.ReadLine()) != null) {
lCount++;
if (Suspicious(line)) {
Console.WriteLine(
"{0}: {1}", lCount, line);
}
}
}

bool Suspicious(string line){
if (MatchNotKeyword(line, blockPrefix) == true) {
return true;
}
if (MatchNotKeyword(line, methodDef) == true) {
return true;
}

Chapter 12: I/O in C# 507
return false;
}

bool MatchNotKeyword(string line, Regex re){
if (re.IsMatch(line)) {
Match m = re.Match(line);
string identifier = m.Groups["id"].Value;
if (Array.IndexOf(keyWords, identifier) < 0) {
return true;

}
}
return false;
}
}///:~

The Main( ) generates a list of all the C# files in the current directory and for
each one creates a CapStyle instance, runs CapStyle.Check( ) and then
CapStyle.Close( ).
Each CapStyle instance contains a list of C# keywords that are allowed to be in
lowercase, as well as instance variables that hold the two regular expressions, and
a StreamReader instance variable that reads the underlying file. The
CapStyle( ) constructor opens the file and constructs the two two regular
expressions. The expressions will match namespaces, class and interface
identifiers, properties, and method names that precede a ‘{‘ character (handling
multiline bracketing conventions is another exercise!). Additionally, the
expressions use group naming to associate the word that begins with a lowercase
letter to a regex variable called id (“
(?<id>[a-z][\w]*)” is the relevant
notation; the parentheses specify the group, the
?<id> specifies the name).
The Check( ) method goes through the StreamReader line-by-line, seeing if
Suspicious( ) returns true; if so, that line is output to the console.
Suspicious( ) in turn calls MatchNotKeyword( ), passing in the suspect line
and a reference to one of the two instance Regexs. MatchNotKeyword( )
checks for a match; if there is one it assigns the value of the Regex group named
id to the string identifier. If this string does not appear in the array of C#
keywords, MatchNotKeyword( ) returns true, which causes Suspicious to
return true to Check( ).
In addition to not handling multiline bracketing, this program sometimes marks

strings that contain formatting brackets incorrectly. If you improve the program,
please drop the authors a line at www.ThinkingIn.Net.

508 Thinking in C# www.MindView.net
Summary
The .NET IO stream library does satisfy the basic requirements: you can perform
reading and writing with the console, a file, a block of memory, or even across the
Internet (as you will see in Chapter 18). With inheritance, you can create new
types of input and output objects.
The IO library brings up mixed feelings; it does the job and it uses the Decorator
pattern to good effect. But if you don’t already understand the Decorator pattern,
the design is nonintuitive, so there’s extra overhead in learning and teaching it.
There are also some poor choices in naming and implementation issues.
However, once you do understand the fundamentals of Streams and the
Decorator pattern and begin using the library in situations that require its
flexibility, you can begin to benefit from this design, at which point its cost in
extra lines of code will not bother you at all.
Exercises
1. Open a text file so that you can read the file one line at a time. Read each
line as a string and place that string object into a SortedList. Print all
of the lines in the SortedList in reverse order.
2. Modify the previous exercise so that the name of the file you read is
provided as a command-line argument.
3. Modify the previous exercise to also open a text file so you can write text
into it. Write the lines in the SortedList, along with line numbers, out to
the file.
4. Modify Exerise 2 to force all the lines in the SortedList to upper case and
send the results to the console.
5. Modify Exercise 2 to take additional command-line arguments of words
to find in the file. Print all lines in which any of the words match.

6. Modify DirList.cs to actually open each file and only list those files
whose contents contain any of the words specified on the command-line.
7. Modify WordCount.cs so that it produces an alphabetic sort.
8. Write a program that compares the performance of writing to a file when
using buffered and unbuffered I/O.

Chapter 12: I/O in C# 509
9. Write a program that changes operators within a C# source code file (for
instance, that changes addition operators into subtraction, or flips binary
tests from true to false). Use this program to explore mutation testing,
which starts from the premise that every operator ought to affect the
behavior of the program.
10. Write a program that creates Markov chains. First, write a program that
reads each word in a series of files and stores, for each word, the words
that follow it and the probability of that word being next (for instance,
the word “.Net” is likely to be followed by the words “framework” or
“platform” more often than being followed by the word “crepuscular”).
Once this data structure is created from a large enough corpus, generate
new sentences by picking a common word, choosing a successor
probabilistically (use Random.NextDouble( ) and the fact that all
probabilities sum to 1). Run the program on different source texts (press
releases, Hemingway short stories, books on computer programming).
11. Incorporate punctuation, sentence length, and Markov chains longer
than a single word into the previous example.


511
13: Reflection and
Attributes
The idea of run-time type identification (RTTI) seems

fairly simple at first: It lets you find the exact type of an
object when you only have a reference to the base type.
However, the need for RTTI uncovers a whole plethora of interesting (and often
perplexing) OO design issues, and raises fundamental questions of how you
should structure your programs.
This chapter looks at the ways that C# allows you to add and discover
information about objects and classes at run-time. This takes three forms:
“traditional” RTTI, which assumes that you have all the types available at
compile-time and run-time, the “reflection” mechanism, which allows you to
discover class information solely at run-time, and the “attributes” mechanism,
which allows you to declare new types of “meta-information” with a program
element and write programs that recognize and work with that new meta-
information. We’ll cover these three mechanisms in order.
The need for RTTI
Consider the now familiar example of a class hierarchy that uses polymorphism.
The generic type is the base class Shape, and the specific derived types are
Circle, Square, and Triangle:

512 Thinking in C# www.MindView.net

Figure 13-1: The Shape hierarchy
This is a typical class hierarchy diagram, with the base class at the top and the
derived classes growing downward. The normal goal in object-oriented
programming is for the bulk of your code to manipulate references to the base
type (Shape, in this case), so if you decide to extend the program by adding a
new class (Rhomboid, derived from Shape, for example), the bulk of the code
is not affected. In this example, the dynamically bound method in the Shape
interface is Draw( ), so the intent is for the client programmer to call Draw( )
through a generic Shape reference. Draw( ) is overridden in all of the derived
classes, and because it is a dynamically bound method, the proper behavior will

occur even though it is called through a generic Shape reference. That’s
polymorphism.
Thus, you generally create a specific object (Circle, Square, or Triangle),
upcast it to a Shape (forgetting the specific type of the object), and use that
Shape abstract data type reference in the rest of the program.
As a brief review of polymorphism and upcasting, you might code the above
example as follows:
//:c13:Shapes.cs
using System;
using System.Collections;

class Shape {
internal void Draw() {
Console.WriteLine(this + ".Draw()");
}
}

class Circle : Shape {
Shape

Draw()

Circle

Square

Triangle


Chapter 13: Reflection and Attributes 513

public override string ToString() {
return "Circle";}
}

class Square : Shape {
public override string ToString() {
return "Square";}
}

class Triangle : Shape {
public override string ToString() {
return "Triangle";}
}

public class Shapes {
public static void Main() {
IList s = new ArrayList();
s.Add(new Circle());
s.Add(new Square());
s.Add(new Triangle());
IEnumerator e = s.GetEnumerator();
while (e.MoveNext()) {
((Shape)e.Current).Draw();
}
}
} ///:~

The base class contains a Draw( ) method that indirectly uses ToString( ) to
print an identifier for the class by passing this to Console.WriteLine( ). If that
function sees an object, it automatically calls the ToString( ) method to produce

a String representation.
Each of the derived classes overrides the ToString( ) method (from object) so
that Draw( ) ends up printing something different in each case. In Main( ),
specific types of Shape are created and then added to an IList. This is the point
at which the upcast occurs because the IList holds only objects. Since
everything in C# is an object, an IList can also hold Shape objects. But during
an upcast to object, it also loses any specific information, including the fact that
the objects are Shapes. To the ArrayList, they are just objects.
At the point you fetch an element out of the IList’s IEnumerator with
MoveNext( ), things get a little busy. Since the IList holds only objects,

514 Thinking in C# www.ThinkingIn.NET
MoveNext( ) naturally produces an object reference. But we know it’s really a
Shape reference, and we want to send Shape messages to that object. So a cast
to Shape is necessary using the traditional “(Shape)” cast. This is the most
basic form of RTTI, since in C# all casts are checked at run-time for correctness.
That’s exactly what RTTI means: At run-time, the type of an object is identified.
In this case, the RTTI cast is only partial: The object is cast to a Shape, and not
all the way to a Circle, Square, or Triangle. That’s because the only thing we
know at this point is that the IList is full of Shapes. At compile-time, this is
enforced only by your own self-imposed rules, but at run-time the cast ensures it.
Now polymorphism takes over and the exact method that’s called for the Shape
is determined by whether the reference is for a Circle, Square, or Triangle.
And in general, this is how it should be; you want the bulk of your code to know
as little as possible about specific types of objects, and to just deal with the
abstract data type that represents a family of objects (in this case, Shape). As a
result, your code will be easier to write, read, and maintain, and your designs will
be easier to implement, understand, and change. So polymorphism is the general
goal in object-oriented programming.
But what if you have a special programming problem that’s easiest to solve if you

know the exact type of a generic reference? For example, suppose you want to
allow your users to highlight all the shapes of any particular type by turning them
purple. This way, they can find all the triangles on the screen by highlighting
them. Or perhaps you have an external method that needs to “rotate” a list of
shapes, but it makes no sense to rotate a circle so you’d like to skip only the circle
objects. This is what RTTI accomplishes: you can ask a Shape reference the exact
type that it’s referring to. With RTTI you can select and isolate special cases.
The Type object
To understand how RTTI works in C#, you must first know how type information
is represented at run-time. This is accomplished through a special kind of object
called the Type object, which contains information about the class. (This is
sometimes called a meta-class.) In fact, the Type object is used to create all of
the “regular” objects of your class
1
.
There’s a Type object for each type that is part of your program. That is, each
time you write and compile a new type, whether it be a value type such as a
structure, or a “real” object, a single Type object is created. A collection of Type


1
In fact, all your objects will be of type RuntimeType, which is a subtype of Type.

Chapter 13: Reflection and Attributes 515
objects is stored in binary format in an assembly (usually having an extension of
.dll or .exe). At run-time, when you want to make an object of that type, the CLR
first checks to see if the Type has been instantiated within the current
AppDomain (roughly, an AppDomain is the runtime container for the
assemblies of a single application). If the type has not been instantiated, the CLR
reads the assembly and transforms the CIL contents into machine instructions

appropriate to the local hardware (this process is called Just In Time
Compilation, and JIT has become a common verb to describe it). This happens in
every AppDomain that uses the Type; some amount of memory efficiency is
traded for the benefits, such as security, that come from isolating AppDomains.
Thus, a .NET program isn’t completely loaded before it begins, which is different
from many traditional languages.
Once the Type object for that type is in memory, it is used to create all instances
of that type.
If this seems shadowy or if you don’t really believe it, here’s a demonstration
program to prove it:
//:c13:SweetShop.cs
// Examination of the way type loading works.
using System;

class Candy {
static Candy(){
Console.WriteLine("Candy loaded");
}
}

class Gum {
static Gum(){
Console.WriteLine("Gum loaded");
}

internal static string flavor = "juicyfruit";
}

class Cookie {
static Cookie() {

Console.WriteLine("Cookie loaded");
}
}

516 Thinking in C# www.MindView.net

public class SweetShop {
public static void Main() {
Console.WriteLine("Inside Main");
new Candy();
Console.WriteLine("After creating Candy");
Type t = Type.GetType("Gum");
Console.WriteLine(
"After Type.GetType(\"Gum\")");
Console.WriteLine(Gum.flavor);
Console.WriteLine("Before creating Cookie");
new Cookie();
Console.WriteLine("After creating Cookie");
}
} ///:~


Each of the classes Candy, Gum, and Cookie have a static constructor that is
executed the first time an instance of the class is created. Information will be
printed to tell you when that occurs. In Main( ), the object creations are spread
out between print statements to help detect the time of loading.
A particularly interesting sequence is:
Type t = Type.GetType("Gum");
Console.WriteLine(
"After Type.GetType(\"Gum\")");

Console.WriteLine(Gum.flavor);


Type.GetType( ) is a static method that attempts to load a type of the given
name. A Type object is like any other object and so you can get and manipulate a
reference to it. One of the ways to get a reference to the Type object is
Type.GetType( ), which takes a string containing the textual name of the
particular class you want a reference for.
When you run this program, the output will be:
Inside Main
Candy loaded
After creating Candy
After Type.GetType("Gum")
Gum loaded
juicyfruit

Chapter 13: Reflection and Attributes 517
Before creating Cookie
Cookie loaded
After creating Cookie

You can see that each Class object is loaded only when it’s needed, and the static
constructor is run immediately prior to when data from the Type is needed (in
this case, the static string that told the Gum’s flavor). This is in slight contrast
to Java, which instantiates the static state of a type immediately upon class
loading.
Type retrieval operator
C# provides a second way to produce the reference to the Type object, using the
type retrieval operator typeof( ). In the above program this would look like:
typeof(Gum);


which is not only simpler, but also safer since it’s checked at compile-time.
Because it eliminates the method call, it’s also more efficient.
Checking before a cast
So far, you’ve seen RTTI forms including:
♦ The classic cast; e.g., “(Shape),” which uses RTTI to make sure the cast
is correct.
♦ The Type object representing the type of your object. The Type object
can be queried for useful run-time information.
In C++, the classic cast “(Shape)” does not perform RTTI. It simply tells the
compiler to treat the object as the new type. In C#, which does perform the type
check, this cast is often called a “type safe downcast.” The reason for the term
“downcast” is the historical arrangement of the class hierarchy diagram. If
casting a Circle to a Shape is an upcast, then casting a Shape to a Circle is a
downcast. However, you know a Circle is also a Shape, and the compiler freely
allows an upcast assignment, but you don’t know that a Shape is necessarily a
Circle, so the compiler doesn’t allow you to perform a downcast assignment
without using an explicit cast.
There’s one more form of RTTI in C#. These are the keyword is and as. The
keyword is tells you if an object is an instance of a particular type. It returns a
bool so you use it in the form of a question, like this:
if(cheyenne is Dog)
((Dog)cheyenne).Bark();


518 Thinking in C# www.ThinkingIn.NET
The above if statement checks to see if the object cheyenne belongs to the class
Dog before casting cheyenne to a Dog. It’s important to use is before a
downcast when you don’t have other information that tells you the type of the
object; otherwise you’ll end up with an InvalidCastException.

The keyword as performs a downcast to the specified type, but returns null if the
object is not an object of the specified type. So the above example becomes:
Dog d = cheyenne as Dog;
d.Bark();

If the object cheyenne did not belong to class Dog, this would still compile fine,
but you would get a NullReferenceException when the line d.Bark( )
attempts to execute. You should exercise extreme caution with as, especially if
you do not immediately attempt to use the result. Leaving possibly null values
floating around is sloppy programming.
Ordinarily, you will be hunting for one type (triangles to turn purple, for
example), but you can easily tally all of the objects using is. Suppose you have a
family of Pet classes:
//:c13:Pets.cs
class Pet { }
class Dog :Pet { }
class Pug :Dog { }
class Cat :Pet { }
class Rodent :Pet { }
class Gerbil :Rodent { }
class Hamster :Rodent { }
///:~

Using is, all the pets can be counted:
//:c13:PetCount1.cs
//Compile with:
//csc Pets.cs PetCount1.cs
using System;
using System.Collections;


public class PetCount {
static string[] typenames = {
"Pet", "Dog", "Pug", "Cat",
"Rodent", "Gerbil", "Hamster",
};

Tài liệu bạn tìm kiếm đã sẵn sàng tải về

Tải bản đầy đủ ngay
×