tags:

views:

152

answers:

3

This is a question for the generic collection gurus.

I'm shocked to find that TList does not override equals. Take a look at this example:

list1:=TList<String>.Create;       
list2:=TList<String>.Create;

list1.Add('Test');
list2.Add('Test');

Result:=list1.Equals(list2);

"Result" is false, even though the two Lists contain the same data. It is using the default equals() (which just compares the two references for equality).

Looking at the code, it looks like the same is true for all the other generic collection types too.

Is this right, or am I missing something??

It seems like a big problem if trying to use TLists in practice. How do I get around this? Do I create my own TBetterList that extends TList and overrides equals to do something useful? Or will I run into further complications with Delphi generics...... ?

[edit: I have one answer so far, with a lot of upvotes, but it doesn't really tell me what I want to know. I'll try to rephrase the question]

In Java, I can do this:

List<Person> list1=new ArrayList<Person>();
List<Person> list2=new ArrayList<Person>();
list1.add(person1);
list2.add(person1);
boolean result=list1.equals(list2);

result will be true. I don't have to subclass anything, it just works.

How can I do the equivalent in Delphi?

If I write the same code in Delphi, result will end up false.

If there is a solution that only works with TObjects but not Strings or Integers then that would be very useful too.

+7  A: 

Generics aren't directly relevant to the crux of this question: The choice of what constitutes a valid base implementation of an Equals() test is entirely arbitrary. The current implementation of TList.Equals() is at least consistent will (I think) all other similar base classes in the VCL, and by similar I don't just mean collection or generic classes.

For example, TPersistent.Equals() also does a simple reference comparison - it does not compare values of any published properties, which would arguably be the semantic equivalent of the type of equality test you have in mind for TList.

You talk about extending TBetterList and doing something useful in the derived class as if it is a burdensome obligation placed on you, but that is the very essence of Object Oriented software development.

The base classes in the core framework provide things that are by definition of general utility. What you consider to be a valid implementation for Equals() may differ significantly from someone else's needs (or indeed within your own projects from one class derived from that base class to another).

So yes, it is then up to you to implement an extension to the provided base class that will in turn provide a new base class that is useful to you specifically.

But this is not a problem.

It is an opportunity.

:)

You will assuredly run into further problems with generics however, and not just in Delphi. ;)

Deltics
Thanks for such a nicely written answer. I find your views slightly perplexing. A few points: 1) TStringList has an equals method that behaves exactly as I describe, so there is a precedent in the VCL for this, but perhaps you have a point here 2) The default equals() should satisfy the most commonly required behaviour. For customised behaviour you can use TComparer (which is what I've ended up doing in this case, as the default implementation is useless) 3) Comparing two lists for equality is hardly a specific requirement. I would expect a decent collections library to provide it.
awmross
TList<T> is different to a TStringList (and different, semantically, to a TList<String>). One is a class that could be a list of *any type* - a string is only one such thing. A TStringList is only ever a list of strings. One TStringList can sensibly be said to be the same as another TStringList if they contain the same strings, in the same order. But note that TStrings.Equals() takes no account of any associated Object[] data, which might be significant in a particular case (and possibly considered a flaw/problem/bug in the current implementation by *some* people).... (continued...)
Deltics
...(continued). You suggest that TList<T>.Equals should do a simple comparison of each <T> element it contains... that works for strings and integers, but what about other TObjects derived types? What about record types? Interfaces? In those cases the required comparisons are more problematic and a generic solution is not reasonably possible. To that extent Generics may be considered the cause of the problem in this case, but that is only because they are an even *more* generalised case than is usual in non-Generic base classes.
Deltics
Isn't that exactly what Java's ArrayList<T> does? http://download-llnw.oracle.com/javase/1.5.0/docs/api/java/util/AbstractList.html#equalsAt least it does this for Objects (you can't store primitives in a List). "What about record types? Interfaces?" What if we restrict the Type T to be a descendant of TObject? We could then just call TObject.equals on each associated pairing of TObjects in the TList. (Is it inappropriate to just say "Objects in the List"? It rolls off the tongue a lot easier) Disclaimer: I only know enough generics and enough Delphi to be dangerous
awmross
Awmross, the original problem is that TList.Equals won't do a deep comparison of the list's contents. Deltics correctly points out that the reason it can't is that there is no way to write code to perform generic comparison of all the types TList might hold. Your suggested solution is to limit the types it can hold, but that would *break TList*! (Note that that wouldn't even solve your original problem since since string doesn't descend from TObject.)
Rob Kennedy
+1  A: 

What it boils down to is this:

In Java (and .NET languages) all types descend from Object. In Delphi integers, strings, etc. do not descend from TObject. They are native types and have no class definition.

The implications of this difference are sometimes subtle. In the case of generic collections Java has the luxury of assuming that any type will have a Equals method. So writing the default implementation of Equals is a simple matter of iterating through both lists and calling the Equals method on each object.

From AbstractList definition in Java 6 Open JDK:

public boolean equals(Object o) {
    if (o == this)
        return true;
    if (!(o instanceof List))
        return false;

    ListIterator<E> e1 = listIterator();
    ListIterator e2 = ((List) o).listIterator();
    while(e1.hasNext() && e2.hasNext()) {
        E o1 = e1.next();
        Object o2 = e2.next();
        if (!(o1==null ? o2==null : o1.equals(o2)))
            return false;
    }
    return !(e1.hasNext() || e2.hasNext());
}

As you can see the default implementation isn't really all that deep a comparison after all. You would still be overriding Equals for comparison of more complex objects.

In Delphi since the type of T cannot be guaranteed to be an object this default implementation of Equals just won't work. So Delphi's developers, having no alternative left overriding TObject.Equals to the application developer.

codeelegance
So the fact that TList can store primitives as well as Objects (unlike the Java version, which is restricted to Objects only) makes comparing members of the TList more difficult. The DeHL library has implemented an equals() that works for all generic types (see my accepted answer). I don't know how it works, but I'm guessing it uses RTTI (i.e. reflection) to discover the types and use the appropriate comparison. After all, you can always get around the type system using reflection if required. So I'm not sure it's impossible to do this; I guess the Delphi developers just decided not to.
awmross
@awmross This may be something they try in the future. RTTI was greatly improved in Delphi 2010 but was a bit cumbersome in Delphi 2009 when they introduced generics.
codeelegance
@codeelegance Reading further I see that RTTI in 2009 only worked on Published items. That sure is cumbersome! And limited. The generic collections were introduced in 2009? (I skipped straight from 2006 to 2010). So a generic equals would have been impossible, as you say. I wonder if they can change the implementation of equals in TList now or if it is too late to do so; for backwards compatibility reasons.
awmross
You can use the TypeInfo(T) operator in a generic class to get the type information for the "yet-undefined-type". Then, you can decide what comparator to use, etc. It gets complicated at some point. DeHL defines this stuff in DeHL.Types. You can check out how exactly it works.
alex
+1  A: 

I looked around and found a solution in DeHL (an open source Delphi library). DeHL has a Collections library, with its own alternative List implementation. After asking the developer about this, the ability to compare generic TLists was added to the current unstable version of DeHL.

So this code will now give me the results I'm looking for (in Delphi):

list1:=TList<Person>.Create([Person.Create('Test')]);
list2:=TList<Person>.Create([Person.Create('Test')]);

PersonsEqual:=list1.Equals(list2); // equals true

It works for all types, including String and Integer types

stringList1:=TList<string>.Create(['Test']);
stringList2:=TList<string>.Create(['Test']);

StringsEqual:=stringList1.Equals(stringList2); // also equals true

Sweet!

You will need to check out the latest unstable version of DeHL (r497) to get this working. The current stable release (0.8.4) has the same behaviour as the standard Delphi TList.

Be warned, this is a recent change and may not make it into the final API of DeHL (I certainly hope it does).

So perhaps I will use DeHL instead of the standard Delphi collections? Which is a shame, as I prefer using standard platform libraries whenever I can. I will look further into DeHL.

awmross