I love .NET. It has
some great features and some welcome improvements over Java.
However, when .NET goes wrong, it goes VERY wrong. This page
is dedicated to the huge fuckups that (I think) Microsoft
"engineers" have made.
This page is here in the hope that it will draw enough attention to
force Microsoft into fixing the problems in .NET V2. (Probably
not going to happen though *sigh*).
.NET was designed with OO in mind, but I think someone forgot to
tell the guys who wrote the base class library. Maybe
Microsoft should have called the base class library the
highly-coupled, not-so-cohesive, not-so-common
class library ;-).
Some of you might be interested to know that I was a big Microsoft
supporter for years. Once upon a time, before
Java 1.2, Microsoft had the most
popular and well supported object-based architecture around (COM)
and I loved them for it.
I now have a comparison of Java and C#/.NET
here.
C#
- Problem:
No anonymous inner classes.
Anonymous inner classes are the OO equivalent of lambda
abstractions and can be very powerful.
C# features type-safe function pointers called delegates but has
NO concept of anonymous inner delegates. This makes Java's
event handling with anonymous inner classes much more convenient
than C#'s named delegates.
Solution:
Add support for inner and anonymous inner classes (J# supports
them! Jesus!) as well as anonymous methods.
The syntax for anonymous methods could look like this:
public MyForm()
{
this.Click +=
new EventHandler(object sender,
EventArgs eventArgs)
{
Close();
};
}
|
The code without
anonymous methods would look like this:
public MyForm()
{
this.Click +=
new EventHandler(MyClickHandler);
}
private
void MyClickHandler(object
sender, EventArgs eventArgs)
{
Close();
}
|
It may not look
impressive but trust me, many event handlers are only one line
long and writing a member function for each handler isn't pretty.
Making event handler methods part of the class often makes no
sense because the event handling code often has no context outside
of the method that adds the handler. Classes are
often less cohesive when they contain event handlers as top
level methods. Anonymous methods increase
encapsulation and localises the method to the area where it is
used.
[Update: 18 November 2002: Microsoft have
announced that a future version of C# will include support for
anonymous methods.]
It will probably be a cold day in hell before Microsoft adds
support for anonymous inner classes. The reason is simple,
Java has them and "newbie" Java developers love to complain
about how confusing they are. Microsoft will not add the
feature in an attempt to gain respect from the developers who are
confused by the language feature in Java.
Anonymous inner classes (like anonymous methods) increases
encapsulation and cohesion and would make a good addition to C#.
- Problem:
The syntax for using structs and classes look the same but,
semantically, they can mean very different things.
e.g. The following code will behave differently depending on
whether or not Point is a class or struct.
Point point1 = new Point(10, 10);
Point point2 = point1;
point2.X = 20;
Console.WriteLine(point1.X);
|
If Point is a
struct, changing point2.X won't have an effect on point1.X.
This IMO is the most fundamental flaw in C#.
Solution:
One solution would be to require the use of the struct
keyword when referring to value types. Intrinsic value types
(int, short, long, etc) can ignore this requirement.
public
static void Foo(struct
Point p)
{
// I can see that p is a struct so I know I have a
copy and not a reference.
p.x = 20;
}
public
static void Main()
{
// I know p is stack allocated
because I'm using 'new struct' instead of 'new'.
struct Point p = new
struct Point(10, 10);
// I can see that p is a struct
so I know I'm passing Foo() a copy of p.
Foo(p);
}
|
Using the struct keyword
also conveniently eliminates the confusion that C++ programmers
have over the use of the new keyword to construct value
types [In C++ new is used to allocate objects on the heap -- never
the stack]. This solution is so obvious (especially when you
consider C#'s C++ ancestry), elegant and unobtrusive that it's
amazing that Microsoft missed or dismissed it.
System.IO
- The Environment
class contains the static property CurrentDirectory.
Setting this property changes the current directory of the
process. This is a very poor design decision.
Changing the current directory for the entire process has major
consequences for multi-threaded and multi-appdoman applications.
Ignoring the issue of how sensible (or not) it is to allow the
current directory to change, there is another problem: the use
of a property setter to change global state. Microsoft's
own documentation states that a set method should be used
in place of a property setter if setting the property
will do something major.
Sun, being thoughtful engineers, have not added the ability to
change the current directory in Java. This means that in
the future, applications can all run under one VM without the
random chaotic state changes that .NET applications will have to
endure.
Solution:
Make the CurrentDirectory property a read only property.
Add a new method called SetCurrentDirectory. Document the
method as potentially dangerous in multi-threaded applications.
-
FileSystemInfo.Refresh throws an
ArgumentException if the underlying file doesn't exist.
This makes no sense because FileInfo.Refresh takes no
arguments.
Solution:
FileSystemInfo.Refresh
should adapt ArgumentExceptions into
FileNotFoundExceptions before throwing them.
System.Windows.Forms
- Problem:
The standard controls such as the TreeView, ListBox and Combo
box don't separate the UI from the data. The controls store
the data they display themselves rather than requesting the data
from a model as required. This way of doing things
encourages developers to use the UI controls as a way to store
their data (something no self-respecting OO programmer would even
begin to consider).
Solution 1:
Use the Model-View-Controller architecture to separate the
concerns of data display and data storage. This has the benefit of
eliminating the need to store data in two locations (the UI and
the storage classes) and releases the developer from having to
manually populate and update the UI control.
Solution 2:
Use DataBinding to seperate the UI from the data. This
method won't work with TreeViews.
Example of why M-V-C is superior and Windows Forms sucks:
Say we need to use a ListBox control to display the contents of an
ArrayList. The problem we're immediately faced with is how
to communicate to the ListBox the contents of the ArrayList.
The Microsoft way would be to manually walk through the ArrayList
and for every item in the ArrayList add it to the ListBox.
This means that the ListBox, internally, contains a a copy of
every item in our ArrayList. The ListBox contains data and
this breaks one of the first rules of UI design. Containing
data means that the ListBox is slower and takes twice as much
memory. If our ArrayList contained 10000 items, the ListBox
would also need to use memory to store those 10000 items.
If we also wanted to display the items in the ArrayList in a
ComboBox and ListView at the same time it would use three times as
much memory. Additionally, there is a huge problem with data synchronization.
If you remove an item from the ArrayList you have to remember to
update the ListBox (this includes having to write the code to find
the item in the list box and update it).
The OO solution is simple and elegant. So simple in fact
that it would make Windows Forms much easier to implement (for
Microsoft) and use (for developers). ListBoxes no longer
need to store and maintain an internal list of items they need to
display. You just need to define the following interface:
public interface IListModel
{
ListModelEventHandler event Added(ListModelEventArgs e);
ListModelEventHandler event Removed(ListModelEventArgs e);
ListModelEventHandler event Changed(ListModelEventArgs e);
int Count
{
get;
}
object GetAt(int index);
}
|
The ListBox control would
no longer store the data it displays. The ListBox would
simply be passed an instance of IListModel. Every time the
ListBox needs to render an item in the list, it would consult the
IListModel instance and ask it for the item to draw.
This means that the ListBox never stores any data (potentially
saving a lot of memory). It also means you don't have to write
complex algorithms to keep your data and the UI control (the ListBox) in sync. You simply have to fire an event from the
model every time you think your data may have changed. This
is much simpler than having to search the ListBox and modify the
appropriate items. The other cool thing about using MVC is
that your data doesn't even have to exist in memory at all.
You can write a model that returns calculated values. For
example, you can write a model that returns "0" as the item at
index 0 and "1" as the item at index "1" etc. This allows
you to make a ListBox that displays and looks like it contains
2^32 numbers without every needing to store all those numbers.
If you tried to do that using Windows Forms your program would
most likely lock up and run out of memory.
Example of using MVC based ListBox, ListView and ComboBox to display all integers:
class IntegerModel
: AbstractListModel
{
public override
int Count
{
return int.Max;
}
public override GetAt(int
index)
{
return index;
}
}
IListModel model = new
IntegerModel();
ListBox
listbox = new ListBox(model);
ListView listview = new
ListView(model);
ComboBox combobox = new
ComboBox(model);
|
Example of using Microsoft's ListBox, ListView and ComboBox to display all integers:
ListBox listbox = new ListBox();
for (int
i = 0; i < int.Max; i++)
{
listbox.Items.Add(i);
}
ListView listview = new
ListView();
for (int
i = 0; i < int.Max; i++)
{
listview.Items.Add(i);
}
ComboBox combobox = new
ComboBox();
for (int
i = 0; i < int.Max; i++)
{
combobox.Items.Add(i);
}
|
The code using the MVC/OO listbox
is
clearer, declarative and allows us to easily and dynamically change
the values displayed. We only have to declare
that we want to display all the integers once (not three times).
Doing it the Microsoft way would result in repetitive code, lots of memory use, hard disk thrashing
and eventually an OutOfMemoryException and even if it did work,
it would be a pain to maintain.
- Problem:
The Panel control is intended to be used
as a container for other controls yet it derives from
ScrollableControl rather than ContainerControl (ContainerControls are
ScrollableControls but ScrollableControls aren't
ContainerControls). Who
does Microsoft hire to design this crap?
Control ->
ScrollableControl -> ContainerControl
|
-> Panel
The design makes it look like Panel isn't a
container control when it reality that's the Panel control's
primary purpose -- being a general purpose container for other
controls.
Solution:
Make Panel derive from ContainerControl.
Control ->
ScrollableControl -> ContainerControl -> Panel
- Problem:
The AnchorStyles and DockingStyle
enumerations are inconsistently named. The enumerations
are so closely related you have to wonder how the hell anyone
could make a mistake like this.
(Yes I do realise that AnchorStyles is a [Flags] enumeration)
Solution:
Rename AnchorStyles to AnchorStyle.
- Problem:
Layout management support is poor. Every control has a
DockStyle and AnchorStyle property. This is bad cause it
means that each control *knows* about where it will sit.
This should be left up to a different class (the LayoutManager).
Solution:
Use the
strategy design pattern. Every ContainerControl
should have a LayoutManager property which set/gets the class
responsible for laying out child controls.
A default layout manager that supports the classic Anchor-Dock
style can be provided to support VB programmers.
This solution replaces enumerations (which are fixed) with an
extensible way of adding support for new layout algorithms.
- Problem:
Most controls only have the default parameter-less constructor. For
example, Button doesn't have constructors that takes the button
text. This was probably overlooked cause the form designer
doesn't need it.
Some of us like to program GUIs manually with code rather than by
dragging and dropping though.
Solution:
Add useful constructors to the standard controls.
- Problem:
There is no easy way for a control to capture the keyevents from
its child controls (and their child controls etc). In
Java/Swing, this is a simple flag you set when you register the
action listener.
Forms have the KeyPreview property which lets you do
this for top level windows but not controls.
KeyPreview is also pretty lame compared to what Swing lets
you do.
Solution:
Add support for this by adding more advanced key event
registration.
(BTW You can hack around this using IMessageFilter).
- Problem:
ImageLists are used everywhere but they shouldn't
be. Although image lists can improve performance (if you're
still using a 386), they are
much harder to work with than simply setting an "Image" property
for each component.
Good design should come first and optimisation can come later.
There is no reason to drag the old Windows "ImageList"
technology into the .NET era.
-
Solution:
Don't use ImageLists. Everything that has an
Image should have an Image property. Performance can be
regained by internally caching Images.
-
Problem:
Control borders aren't
implemented using the
strategy design pattern. This means it is up to the
control writer to add support for borders themselves. This leads
to more work for control writers and less features and flexibility
for control users.
Solution:
-
Use the
strategy design pattern instead using &*$%@# hardwired
enumerations.
System.Collections
-
Problem:
System.Collections
sucks. There has been an
attempt design decent collection classes but the "engineer" gave
up too soon.
There is IList and IDictionary but there is no IQueue or ISet and
too many classes in the .NET framework rely on concrete
implementations of IList and IDictionary (such as ArrayList and
Hashtable) rather than just use the abstract concepts of just
IList and IDictionary. Any first year computer science
student knows the importance of abstracting abstract data types
(list, dictionary, stack) from their implementation (arraylist,
hashtable, arraystack). One wonders where the guy who
designed System.Collections "earned" his computer science
degree...
-
Solution:
Refer to first year text books (or
Java2 documentation) and write decent collection classes.
-
Problem:
CollectionBase is evil. It exposes its implementation to
deriving classes through the
InnerList : ArrayList
property.
The only time the names of ANY concrete collection class
(ArrayList,
LinkedList,
Queue etc) should be used is on the right hand side of the
new operator.
Solution 1:
Make CollectionBase take an IList in its constructor.
InnerList should be exposed as an IList (not ArrayList) and should
be assigned the value of the IList given in the constructor.
This means CollectionBase can internally use any kind of IList for
storage. [Microsoft probably overlooked this because they
only supply ONE list implementation -
ArrayList. Lame].
Solution 2:
Don't use
CollectionBase.
Make a generic
ListWrapper
class that decorates/wraps ANY
IList
implementing object. Then write a
ListWithEvents
object that extends
ListWrapper.
The
ListWithEvents
object will fire events for every action that
IList
supports (Add/Remove etc). Then use ListWithEvents where-ever you
would normally use your
CollectionBase
derived class. This eliminates the need to create a new type
just to intercept actions that occur on the List. You can
use event handling instead. You can listen to IList actions
of ANY list implementation (ArrayList, LinkedList etc).
-
Problem:
IList doesn't have methods such as Sort or Search but ArrayList does. The operations Sort and
Search are generic list operations and should not be
made specific to a given implementation (such as ArrayList).
If you have an IList implementation that isn't an ArrayList then
there would be no way to perform a sort or Sort on
it. Microsoft's solution is one of the most inelegant I've
ever seen. To sort a LinkedList you would have to call
ArrayList.Adapter to adapt the LinkedList into an
ArrayList. Then you can call the Sort or
BinarySearch method on the adapter. This is absolutely
horrifying design. Both LinkedList and ArrayList are *lists*
which by definition can be searched and sorted using a generic
algorithm. There should be no need to adapt a LinkedList
into an ArrayList to sort the list.
Here's a simple
illustration of the problem:
abstract class Animal
{
public abstract void
Eat();
}
class Cat
{
public override void Walk() {...}
public override void Purr() {...}
}
class Dog
{
public override void Bark() {...}
}
Cat cat = new
Cat();
cat.Walk();
Dog dog = new
Dog();
dog.Walk(); // Can't do this! |
All animals can eat but only cats can walk. Later on in the
design phase we realise that all animals can walk. What do
we do? The natural solution would be to add Walk() to the
Animal class.
abstract class Animal
{
public abstract void
Eat();
public abstract void
Walk();
}
class Cat
{
public override void Walk() {...}
public override void Purr() {...}
}
class Dog
{
public override void Walk() {...}
public override void Bark() {...}
}
Animal cat = new
Cat();
cat.Walk();
Animal dog = new
Dog();
dog.Walk(); // Yah! |
Based on their work with the .NET collection classes, Microsoft's
solution would have been to provide a method that would allow you
to adapt any animal into a cat. You would essentially have
to turn a dog into a cat in order to make it walk.
abstract class Animal
{
public abstract void
Eat();
}
class Cat
{
public override void Walk() {...}
public override void Purr() {...}
public static Cat Adapter(Animal a)
{
// Turn
animals into a cats here.
}
}
class Dog
{
public override void Bark() {...}
}
Cat catdog;
Dog dog = new Dog();
catdog = Cat.Adapter(dog); // Disturbing
catdog.Walk(); |
Shocking isn't it?
Solution 1:
Add Sort and SortedSearch to the IList interface and provide
a default implementations for IList implementers. Sort would
sort the list and SortedSearch would do an "optimized" search
based on the assumption that the list is already sorted.
This solution would allow lists to provide customized sorting and
searching algorithms. Doing a BinarySearch on a LinkedList
makes no sense since lookups have O(N) performance therefore
LinkedLists could implement SortedSearch using a
linear search algorithm where best performance would be O(1),
worst performance would be O(N), average performance would be O(N/2).
ArrayList.Sort -> Uses Quicksort O(N log N)
LinkedList.Sort -> Uses Mergesort
O(N log N))
ArrayList.SortedSearch -> Uses binary search O(lg N)
LinkedList.SortedSearch -> Uses linear search O(N)
Solution 2:
Make a new class called
ListAlgorithms which contains static methods for performing
algorithms such as SortedSearch and Sort on lists.
This solution would allow application developers to choose the
algorithms they want to use but this means that they need to know
the concrete implementation of the list so Solution 1 would be
better.
- Problem:
All arrays in .NET implement System.Collections.IList.
The indexer (IList.Item) is documented to throw
ArgumentOutOfRangeExceptions if the index given is out of range.
The indexer on arrays throws IndexOutOfRangeExceptions instead of
ArgumentOutOfRangeExceptions therefore
breaking the contract with IList. This is
dangerous because code working against ILists may randomly break when
given array instances. This is a good example of why C#
should have checked exceptions (aka throws clauses) which prevents this kind of abstraction destroying, contract breaking
error.
Solution:
Make IList.Item implementers throw IndexOutOfRangeExceptions
instead of ArgumentOutOfRangeExceptions (or vice versa).
System.Drawing
Like everything else in
.NET, the drawing namespace has a distinct lack of the application
of even basic OO principles.
- Saving image objects (System.Drawing.Image)
isn't performed utilizing the
strategy design pattern. The Image.Save method should
take a strategy class that will automatically save the Image any
any desired format. For instance, there could be a
JpegSaveStrategy that would know how to take pixels produced
by the Image (or some class like ImagePixelProducer)
and and convert them to a JPEG stream.
What Microsoft did was make the lame ass Image.Save
method take an ImageFormat. The ImageFormat class
is sealed, fixed and neutered and is pretty much just an enum.
It is the Image class and not the ImageFormat
class that does the saving. This means it is
impossible to support saving images to formats that aren't
supported by Microsoft. It also means that Microsoft can't
add support for new ImageFormats without having to modify both
the
Image and the ImageFormat classes.
The Image, ImageFormat and Graphics classes
are very tightly coupled (which is very bad).
Java's Drawing2D and
imaging APIs are outstandingly designed. Support for new Image
formats (etc) can easily be added without having to modify a
single class in the core API. This is .NOT the case with
.NET.
Microsoft's insistence on not making their classes thin, hacked wrappers
around their native APIs is going to kill them. This will
become starkly apparent as incompatibilities between different
versions of .NET appear. Come on Microsoft, GDI+ was
designed for C++ NOT C#. WAKE UP.
It is clear from looking at the System.Drawing and
System.Windows.Forms namespaces that .NET is NOT and was never intended to
be a cross platform platform; it is is a cross platform
framework. C# will be like C in that it will supply some
basic features and leave the rest of the services (UI, Drawing
etc) up to the native operating system (which will vary from OS to
OS). Java on the other hand is very much a meta operating
system/platform. Java supplies the same APIs on all platforms.
Other
-
Assemblies can't be unloaded from
memory without unloading the owner AppDomain. This makes
dynamic scripting pretty useless unless you want to do lots of
cross-appdomain-marshaling or live with severe memory leaks.
Check out
all the problems everyone has been having because of
this.
-
C# doesn't inherit Java's
"extends" and "implements" keywords which most definitely
enhances code readability. Microsoft's reason for doing this
is to make C# look more like C++. It's more likely that it
was done to make C# look less like Java. As if we didn't
notice *Rolls eyes* .
|