Professional Documents
Culture Documents
Mod 3 Dotnet Notes
Mod 3 Dotnet Notes
Mod 3 Dotnet Notes
}
}
// At the end of the loop, currentMin holds the value of the smallest
// item in the list of parameters, so return this value.
return currentMin;
}
}
Note:
The ArgumentException class is specifically designed to be thrown by a method if the
arguments supplied do not meet the requirements of the method.
To use the Min method to find the minimum of two int variables named first and second,
we can write this:
int[] array = new int[2];
array[0] = first;
array[1] = second;
int min = Util.Min(array);
And to use the Min method to find the minimum of three int variables (named first,
second, and third), write this:
int[] array = new int[3];
array[0] = first;
array[1] = second;
array[2] = third;
int min = Util.Min(array);
We can see that this solution avoids the need for a large number of overloads, but we
have to write additional code to populate the array.
The solution is to get the compiler to write some of this code for the user by using a
params array as the parameter to the Min method.
Note:
Can’t use the params keyword with multidimensional arrays. The cod in the following
example will not compile:
// compile-time error
public static int Min(params int[,] table)
...
Can’t overload a method based solely on the params keyword. The params keyword does
not form part of a method’s signature, as shown in this example. Here, the compiler
would not be able to distinguish between these methods in code that calls them:
// compile-time error: duplicate declaration
public static int Min(int[] paramList)
...
public static int Min(params int[] paramList)
...
Not allowed to specify the ref or out modifier with params arrays, as shown in this
example:
// compile-time errors
public static int Min(ref params int[] paramList)
...
public static int Min(out params int[] paramList)
...
A params array must be the last parameter. (This means that you can have only one
params array per method.) Consider this example:
// compile-time error
public static int Min(params int[] paramList, int i)
...
A non-params method always takes priority over a params method. This means that we
can still create an overloaded version of a method for the common cases, such as in the
following example:
public static int Min(int leftHandSide, int rightHandSide)
...
We can pass the Black.Hle method an actual array. In other words, you can manually
create the array normally generated by the compiler:
object[] array = new object[2];
array[0] = "forty two";
array[1] = 42;
Black.Hle(array);
We can pass the Black.Hle method arguments of different types, and these arguments will
automatically be wrapped inside an object array:
Black.Hle("forty two", 42);
//converted to Black.Hle(new object[]{"forty two", 42});
choice between a method that takes optional parameters and a method that takes a parameter list,
the C# compiler will use the method that takes optional parameters.
Consider the following example:
class Util
{
public static int Sum(params int[] paramList)
{
Console.WriteLine("Using parameter list");
if (paramList == null)
{
throw new ArgumentException("Util.Sum: null parameter list");
}
if (paramList.Length == 0)
{
throw new ArgumentException("Util.Sum: empty parameter
list");
}
int sumTotal = 0;
foreach (int i in paramList)
{
sumTotal += i;
}
return sumTotal;
}
public static int Sum(int param1 = 0, int param2 = 0, int param3 = 0, int
param4 = 0)
{
Console.WriteLine("Using optional parameters");
int sumTotal = param1 + param2 + param3 + param4;
return sumTotal;
}
public static void Main()
{
Console.WriteLine(Util.Sum(2, 4, 6, 8));
//output: Using optional parameters
// 20
Console.WriteLine(Util.Sum(2, 4, 6));
//output: Using optional parameters
// 12
Console.WriteLine(Util.Sum(10, 9, 8, 7, 6, 5, 4, 3, 2, 1));
//output: Using parameter list
// 55
}
What is inheritance?
Inheritance enables the language’s ability to allows to build new class definitions based
on existing class definitions.
Inheritance allows to extend the behavior of a base (or parent) class by enabling a
subclass to inherit core functionality (also called a derived class or child class)
Using inheritance
We declare that a class inherits from another class by using the following syntax:
class DerivedClass : BaseClass
{
...
}
The derived class inherits from the base class, and the methods in the base class become
part of the derived class.
In C#, a class is allowed to derive from, at most, one base class; a class is not allowed to
derive from two or more classes.
However, unless DerivedClass is declared as sealed, we can use the same syntax to
derive other classes that inherit from DerivedClass.
class DerivedSubClass : DerivedClass
{
...
}
Consider an example of declaring the Mammal class as follows. The methods Breathe
are common to all mammals.
class Mammal
{
public void Breathe()
{
...
}
...
}
We could then define new classes for each different type of mammal, adding more
methods as necessary, such as in the following example:
class Horse : Mammal
{
...
public void Trot()
{
...
}
}
class Whale : Mammal
{
...
public void Swim()
{
...
}
}
Note:
We cannot explicitly specify whether the inheritance is public, private, or protected. C#
inheritance is always implicitly public.
use of the colon and that there is no extends keyword.
If we create a Horse object in your application, we can call the Trot, Breathe methods:
Horse myHorse = new Horse();
myHorse.Trot();
myHorse.Breathe();
Similarly, you can create a Whale object, but this time we can call the Swim, Breathe
methods; Trot is not available because it is defined only in the Horse class.
}
...
}
class Horse : Mammal // derived class
{
public Horse(string name) : base(name) // calls Mammal(name)
{
...
}
...
}
If we don’t explicitly call a base-class constructor in a derived-class constructor, the
compiler attempts to silently insert a call to the base class’s default constructor before
executing the code in the derived-class constructor.
Taking the earlier example, the compiler rewrites this
class Horse : Mammal
{
public Horse(string name)
{
...
}
...
}
as this: class Horse : Mammal
{
public Horse(string name) : base()
{
...
}
...
}
This works if Mammal has a public default constructor. However, not all classes have a
public default constructor (for example, remember that the compiler generates a default
constructor only if we don’t write any nondefault constructors), in which case forgetting
to call the correct base-class constructor results in a compile-time error.
Assigning classes
We can declare a variable by using a class type and the new keyword to create an object.
The type-checking rules of C# prevent from assigning an object of one type to a variable
declared as a different type. For example, given the definitions of the Mammal, Horse,
and Whale classes shown here, the code that follows these definitions is illegal:
class Mammal
{
...
}
class Horse : Mammal
{
...
}
class Whale : Mammal
{
...
}
Horse myHorse = new Horse( );
Whale myWhale = myHorse; // error - different types
However, it is possible to refer to an object from a variable of a different type as long as
the type used is a class that is higher up the inheritance hierarchy. So the following
statements are legal:
Horse myHorse = new Horse( );
Mammal myMammal = myHorse; // legal, Mammal is the base class of Horse
If we think about it in logical terms, all Horses are Mammals, we can safely assign an
object of type Horse to a variable of type Mammal.
We can also make a Mammal variable refer to a Whale object. There is one significant
limitation, however: When referring to a Horse or Whale object by using a Mammal
variable, we can access only methods and fields that are defined by the Mammal class.
Any additional methods defined by the Horse or Whale class are not visible through the
Mammal class.
Horse myHorse = new Horse(...);
Mammal myMammal = myHorse;
myMammal.Breathe(); // OK - Breathe is part of the Mammal
class myMammal.Trot(); // error - Trot is not part of the Mammal class
If a base class declares that a method is virtual, a derived class can use the override
keyword to declare another implementation of that method,
The new implementation of the method in the derived class can call the original
implementation of the method in the base class by using the base keyword
Program illustrating virtual and override keywords:
Using System;
public class Shape
// Constructors.
{}
public Hexagon(){ }
Class Test {
// The Circle object did not override the base class implementation of Draw().
static void Main(string[] args)
{
Hexagon hex = new Hexagon("Beth");
hex.Draw();
cir.Draw();
}
}
There are some important rules you must follow when we declare polymorphic methods by
using the virtual and override keywords:
A virtual method cannot be private; it is intended to be exposed to other classes through
inheritance. Similarly, override methods cannot be private because a class cannot change
the protection level of a method that it inherits. However, override methods can have a
special form of privacy known as protected access
The signatures of the virtual and override methods must be identical; they must have the
same name, number, and types of parameters. In addition, both methods must return the
same type
We can only override a virtual method. If the base class method is not virtual and you try
to override it, you’ll get a compile-time error. This is sensible; it should be up to the
designer of the base class to decide whether its methods can be overridden.
If the derived class does not declare the method by using the override keyword, it does
not override the base class method; it hides the method. In other words, it becomes an
implementation of a completely different method that happens to have the same name. As
before, this will cause a compile time warning, which can be silenced by using the new
keyword
An override method is implicitly virtual and can itself be overridden in a further derived
class. However, you are not allowed to explicitly declare that an override method is
virtual by using the virtual keyword.
C# demands that a given class have exactly one direct base class. Therefore, it is not
possible to have a single type with two or more base classes (this technique is known as
multiple inheritance or simply MI).
C# does allow a given type to implement any number of discrete interfaces. In this way, a
C# class can exhibit a number of behaviors while avoiding the problems associated with
classic MI.
It is permissible to configure a single interface to derive from multiple interfaces.
Public items are directly accessible from anywhere, while private items cannot be
accessed from any object beyond the class that has defined it.
When a base class defines protected data or protected members, it is able to create a set of
items that can be accessed directly by any descendent. If you wish to allow the
SalesPerson and Manager child classes to directly access the data sector defined by
Employee, we can update the original Employee class definition as follows:
// Child classes can directly access this information. Object users cannot.
protected string fullName;
The benefit of defining protected members in a base class is that derived types no longer
have to access the data using public methods or properties.
The object user is concerned; protected data is regarded as private (as the user is
“outside” of the family). Therefore, the following is illegal:
Because PTSalesPerson is sealed, it cannot serve as a base class to any other type. If
extended PTSalesPerson, receive a compiler error:
// Compiler error!
The sealed keyword is most useful when creating stand-alone utility classes.
The sealed keyword can also be applied to type members to prevent virtual members
from being further overridden by derived types.
This can be helpful when we do not wish to seal an entire class, just a few select methods
or properties.
For example, PTSalesPerson class to be extended by other classes but make sure those
classes did not further override the virtual GiveBonus(), we could write the following:
...
...
Abstract Classes
Abstract classes are one of the essential behaviors provided by .NET. Commonly, we
would like to make classes that only represent base classes, and don’t want anyone to
create objects of these class types.
We can make use of abstract classes to implement such functionality in C# using the
modifier 'abstract'.
An abstract class means that, no object of this class can be instantiated, but can make
derivations of this.
An example of an abstract class declaration is:
abstract class absClass
{}
An abstract class can contain either abstract methods or non abstract methods. Abstract
members do not have any implementation in the abstract class, but the same has to be
provided in its derived class.
An example of an abstract method:
Also, note that an abstract class does not mean that it should contain abstract members.
Even we can have an abstract class only with non abstract members. For example:
using System;
namespace abstractSample
{
//Creating an Abstract Class
abstract class absClass
{
//A Non abstract method
public int AddTwoNumbers(int Num1, int Num2)
{
return Num1 + Num2;
}
{
return Num1 * Num2;
}
}
}
In the above sample, we can see that the abstract class absClass contains two
methods AddTwoNumbers and MultiplyTwoNumbers. AddTwoNumbers is a non
abstract method which contains implementation andMultiplyTwoNumbers is an abstract
method that does not contain implementation.
The class absDerived is derived from absClass and the MultiplyTwoNumbers is
implemented onabsDerived. Within the Main, an instance (calculate) of the absDerived is
created, and calls AddTwoNumbersand MultiplyTwoNumbers. You can derive an
abstract class from another abstract class. In that case, in the child class it is optional to
make the implementation of the abstract methods of the parent class.
An abstract class cannot be a sealed class. I.e. the following declaration is incorrect.
//Incorrect
abstract sealed class absClass
{
}
//Incorrect
private abstract int MultiplyTwoNumbers();
The access modifier of the abstract method should be same in both the abstract class and
its derived class. If you declare an abstract method as protected, it should be protected in
its derived class. Otherwise, the compiler will raise an error.
An abstract method cannot have the modifier virtual. Because an abstract method is implicitly
virtual.
//Incorrect
public abstract virtual int MultiplyTwoNumbers();
//Incorrect
public abstract static int MultiplyTwoNumbers();
Console.WriteLine("Drawing a 3D Circle");
You figure that a ThreeDCircle “is-a” Circle, so you derive from your existing Circle
type:
public class ThreeDCircle : Circle
Console.WriteLine("Drawing a 3D Circle");
Once you recompile, you find the following warning shown in Visual Studio 2005 (see
Figure below).
1. You could simply update the parent’s version of Draw() using the override keyword.
With this approach, the ThreeDCircle type is able to extend the parent’s default behavior
as required.
2. You can prefix the new keyword to the offending Draw() member of the
ThreeDCircle type. Doing so explicitly states that the derived type’s implemention is
intentionally designed to hide the parent’s version.
Console.WriteLine("Drawing a 3D Circle");
The new can also apply keyword to any member type inherited from a base class (field,
constant, static member, property, etc.).
For example:
Console.WriteLine("Drawing a 3D Circle");
Finally, be aware that it is still possible to trigger the base class implementation of a
shadowed member using an explicit cast (described in the next section). For example:
Extension methods enable you to add methods to existing types without creating a new
derived type, recompiling, or otherwise modifying the original type.
An extension method is a special kind of static method, but they are called as if they were
instance methods on the extended type.
An extension method is a static method of a static class, where the "this" modifier is
applied to the first parameter. The type of the first parameter will be the type that is
extended.
Extension methods are only in scope when you explicitly import the namespace into your
source code with a using directive.
Like: suppose we have a class like bellow:
Now we need to extend the definition of this class, so create a static class to create an
extension method like:
Here a method is NewMethod is created with a parameter using this to define which type
of data need to be extended, now let’s see how to use this function.
1. class Program {
2. static void Main(string[] args) {
3. Class1 ob = new Class1();
4. ob.Display();
5. ob.Print();
6. ob.NewMethod();
7. Console.ReadKey();
8. }
9. }
An interface expresses a behaviour that a given class or structure may choose to support.
Interfaces contain members that don’t have access modifiers. All interface members are
implicitly public and abstract.
int Points{get;}
int GetNumberOfSides();
}
.NET interface types can also define any number of properties.
You cannot instantiate an interface as you would do for a class or structure.
Interfaces are implemented by a class or structure.
Implementing an interface in C#
When a class or structure chooses to extend its functionality by supporting interface
types, it does so by using a comma delimited list in the type definition.
//A class implementing an Interface
{…}
The direct base class must be the first item listed after the colon operator. Say, if we are
implementing System.Object or class MyBaseClass, then it must be the first to
implement.
public class ExampleClass : System.Object,IPointy
{…}
{…}
Implementing an interface is all-or-nothing proposition.
{ return 3; }
public Hexagon() { }
get
{ return 6; }
{
Mamatha A, Asst Prof, Dept of ISE,SVIT Page 30
Module 3 .Net framework for application development
return 6;
Console.WriteLine(hex.Points);
}
We can dynamically determine the set of interfaces supported by a type by making use of
an explicit cast. If a type does not support a requested interface, you receive an
InvalidCastException.
IPointy Ipt;
try
Ipt = (IPointy)hex;
Console.WriteLine(Ipt.Points);
catch(InvalidCastException)
Console.WriteLine(ex.Message);
if(Ipt != null)
Console.WriteLine(Ipt.Points);
else
Console.WriteLine(“Not IPointy”);
If the object in question is not compatible with the specified interface, you are returned
the value false.
If the type is compatible with the interface in question, you can safely call the members
without needing to make use of try/catch logic.
public static void Main()
if(hex is IPointy)
Console.WriteLine(((IPointy)hex).Points);
else
Console.WriteLine(“No IPointy”);
Garbage Collection
One of the great strengths of the Microsoft Visual C# language is that it makes a fundamental
distinction between values and objects. Values and objects are different.
The lifetime of a value is tied to the scope in which it is declared whereas the lifetime of
an object is not tied to the scope in which it is created, as demonstrated by the following
code:
{
int i = 42;
TextBox message = new TextBox();
} // i ends its life here,
// but the object referred to by TextBox lives on
Values typically have very short lifetimes whereas objects typically have long lifetimes.
First we have to allocate some raw memory from the heap. We do this using the
new keyword. You have no control over this phase of an object's creation.
Second we have to convert the raw memory into an object; have to initialize the object.
You do this by using a constructor. In contrast to allocation, you do have control over this
phase of an object's creation.
Object death is also a two-phase process. The two phases exactly mirror the two phases
of creation.
First we have to convert the object back into raw memory. We do this by writing a
destructor. The destructor is the opposite of the constructor.
Second the raw memory has to be given back to the heap; the binary bits that the object
lived in have to be deallocated. Once again you have no control over this phase.
The process of returning memory back to the heap is known as garbage collection.
Writing Destructors
The syntax for writing a destructor is a tilde (~) followed by the name of the class. For
example, here's a simple class that counts the number of live instances by incrementing a
static count in the constructor and decrementing the static count in the destructor:
class Tally
{
public Tally()
{
instanceCount++;
}
~Tally()
{
instanceCount--;
}
public static int InstanceCount()
{
return instanceCount;
}
private static int instanceCount = 0;
}
struct Tally
{
~Tally() { ... } // compile-time error
}
You cannot declare an access modifier (such as public) for a destructor. This is because
you never call the destructor yourself (the garbage collector does, as you'll see shortly).
You never declare a destructor with parameters. Again, this is because you never call the
destructor yourself.
class Tally
{
~Tally() { ... }
}
Into this:
class Tally
{
The compiler-generated Finalize method contains the destructor body inside a try block,
followed by a finally block that calls the base class Finalize. This ensures that a
destructor always calls its base class destructor (just like in C++).
It's important to realize that only the compiler can make this translation. You can't
override Finalize yourself and you can't call Finalize yourself. In other
words, Finalize really is just another name for the destructor.
The fundamental thing to remember about destructors is that we can never call them. The reason
for this is that, in C#, we can never destroy an object yourself. There just isn't any syntax to do it.
There are good reasons why the designers of C# decided to forbid the user from explicitly
writing code to destroy objects. If it was the user’s responsibility to destroy objects, sooner or
later:
We could have forget to destroy the object. This would mean that the object's destructor
(if it had one) would not be run and its memory would not be deallocated back to the
heap. You'd quite easily run out of memory.
We could try and destroy an active object. Remember, objects are held by reference. If a
class held a reference to a destroyed object, it would be a dangling reference. The
dangling reference would end up referring either to unused memory or to a completely
different object. Either way, the outcome of using such a dangling reference would be
undefined. All bets would be off.
We could try and destroy the same object more than once.
These problems are unacceptable in a language like C#, which places robustness and security
high on its list of design goals. Instead, the garbage collector is responsible for destroying objects
Only the garbage collector can destroy objects. The garbage collector guarantees that:
Each object is destroyed regardless of whether your application has explicitly destroyed
all the objects it created. For example, when a program ends, all the current objects will
be destroyed.
Each object is destroyed only once.
Each object is destroyed only when no other references refer to the object. This type of
object is called an unreachable object.
These guarantees are tremendously useful and free, the programmer, from tedious housekeeping
chores that are easy to get wrong. They allow you to concentrate on the logic of the program
itself and be more productive.
However, as with all design trade-offs, garbage collection comes at a price. We don't know the
order in which objects will be destroyed. Objects are not necessarily destroyed in the reverse
order that they are created in (as they are in C++). Neither do we know when the garbage
collector will decide to destroy objects. An object is not destroyed at the moment that it becomes
unreachable. Because destroying objects can be a time-consuming operation, the garbage
collector destroys objects only when it is necessary (when the heap memory is exhausted) or
when we explicitly ask it to (by calling the System.GC.Collect method). Clearly, this makes C#
unsuitable for some time-critical applications.
The garbage collector runs in its own thread and only runs when other threads are in a safe state
(for example, when they are suspended). This is important because, as the garbage collector runs,
it needs to move objects and update object references. The steps that the garbage collector takes
when it runs are as follows:
1. It builds a graph of all reachable objects. It does this by repeatedly following reference
fields inside objects. The garbage collector builds this graph very carefully and makes
sure that circular references do not cause an infinite recursion. Any object not in this
graph must be unreachable.
2. It checks to see if any unreachable objects require finalization. In other words, it checks if
any of the unreachable objects has a destructor. Any unreachable object that requires
finalization is placed in a special queue called the freachable queue (pronounced F-
reachable).
3. The remaining unreachable objects (those that don't require finalization) are deallocated.
The garbage collector performs this deallocation by moving the reachable objects down
the heap, thus defragmenting the heap and freeing memory at the top of the heap. When
the garbage collector moves a reachable object, it also updates any references to the
object.
4. The garbage collector now allows other threads to resume.
5. The unreachable objects that require finalization (now in the freachable queue) are
finalized in a separate thread.
Note:
Writing classes that contain destructors adds complexity to code and to the garbage
collection process and makes your program run more slowly. If your program does not
contain any destructors, the garbage collector does not need to perform Steps 3 and 5 (in
the previous section). Clearly, not doing something is faster than doing it. (There's no
code faster than no code.) The first recommendation is, therefore, to try to avoid using
destructors except when you really need them (for example, consider a using statement
instead; these are covered in the following section).
If we write a destructor, need to write it very carefully. In particular, destructor calls other
objects, those other objects might have already had their destructor called by the garbage
collector (remember, the order of finalization is not guaranteed).
Therefore its better to ensure that each destructor reclaims one resource and nothing else.
This can require splitting a class into two classes (one of which is the class dedicated to
managing the resource).
Resource Management
Sometimes it's inadvisable to release a resource in a destructor; some resources are just
too valuable and too scarce to lie around unreleased for arbitrary lengths of time.
Scarce resources need to be released, and they need to be released as soon as possible.
In these situations, only option is to release the resource yourself.
A disposal method is a method that disposes of a resource. If a class has a disposal
method, we can call it explicitly and thus control when the resource is released.
An example of a class that contains a disposal method is the TextReader class from
the System.IO namespace.
TextReader contains a virtual method called Close.
The StreamReader (which reads characters from a stream) and StringReader (which
reads characters from a string) classes both derive from TextReader and both override
the Close method.
Here's an example that reads lines from a file using the StreamReader class:
TextReader reader = new StreamReader(filename);
string line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}
reader.Close();
It's important to call Close when you have finished with reader to release the file handle
(and encoding) resources.
However, there is a problem with this example; it's not exception-safe. If the call
toReadLine (or WriteLine) throws an exception, the call to Close will not happen; it will
be bypassed.
Exception-Safe Disposal
One way to ensure that a disposal method is always called, regardless of whether there is
an exception, is to call the disposal method inside a finally block. Here's the previous
example using this technique:
TextReader reader = new StreamReader(filename);
try
{
string line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}
}
finally
{
reader.Close();
}
{
type variable = initialization;
try
{
embeddedStatement
}
finally
{
if (variable != null)
{
//calls Dispose( ) method
((IDisposable)variable).Dispose();
}
}
}
The variable you declare in a using statement must be of a type that implements
the IDisposable interface.
The IDisposable interface lives in the System namespace and contains just one method
called Dispose:
namespace System
{
interface IDisposable
{
void Dispose();
}
}
We can use a using statement as a clean, exception-safe, robust way to ensure that a
resource is always automatically released.
The class containing the disposal method (for example, Close in TextReader) implements
the IDisposable interface (as TextReader does).
The class implements Dispose (as it must) to call the disposal method (for
example, TextReader.Dispose calls TextReader.Close).
For example:
Here is the best way to make sure that your code always calls Close on a TextReader:
One of the drawbacks of the disposal method pattern is that it relies on you (the
programmer) to call the disposal method, and programmers tend to occasionally forget to
do things like this.
The trade-off in deciding whether to use destructors or disposal methods is this: A call to
a destructor will happen, just don't know when, whereas you know exactly when a call to
a disposal method happens, just can't be sure that it will actually happen because the user
might forget to call it. However, it is possible to ensure that a disposal method is always
called.
The solution is to call the disposal method from a destructor. This acts as a useful
"backup." We might forget to call the disposal method, but atleast be sure that it will be
called, even if it's only when the program shuts down. An example of how to do this is:
class Example : IDisposable
{
private Resource scarce;
private bool disposed = false;
~Example()
{
Dispose();
}
public virtual void Dispose()
{
if (!disposed)
{
try {
// release scarce resource here
}
finally {
disposed = true;
GC.SuppressFinalize(this);
}
}
}
public void SomeBehavior()
{
checkIfDisposed();
}
private void checkIfDisposed()
{
if (disposed)
{
throw new ObjectDisposedException("Example");
}
}
}
Notice that:
Programs:
using System;
class Car:IDisposable
{
string name;
public Car(string n)
{
name=n;
}
~Car()
{
Console.WriteLine("Within destructor of {0}", name);
class Test
{
public static void Main()
{
Car c1=new Car("One");
Car c2=new Car("Two");
Car c3=new Car("Three");
Car c4=new Car("Four");
c1.Dispose();
c3.Dispose();
}
}
Output:
using System;
class Test
{
public Test()
{}
~Test()
{
Console.WriteLine("Finalizing!!!");
}
public static void Main()
{
Console.WriteLine(“Within Main()");