tags:

views:

337

answers:

7

Is it possible to use C# generics replace these 4 routines with one?

int memcmp (string a, string b){...}
int memcmp (string a, byte[] b){...}
int memcmp (byte[]a,  string b){...}
int memcmp (byte[]a,  byte[] b){...}

I've tried a number of variations, but cannot determine exactly what to use...

For example...

int memcmp<A, B>( A a, B b) 
{
  if ( a.Length < b.Length ) return 1;
  for ( int i = 0 ; i < a.Length ; i++ )
  {
    if ( a[i] != b[i] ) return ( a[i] < b[i] ) ? -1 : 1;
  }
 }

gives the following errors:

  • 'A' does not contain a definition for 'Length'
  • Cannot apply indexing with [] to an expression of type 'A'

Where is a good reference that discusses this?

**NOTE: ** I am not looking for a solution on how to compare strings and bytes, rather seeking an understanding of how generics work in C# using a "proof-of-concept" problem

A: 

Why don't you just use the .Compare function of each type? Each type in C# derives from the base object class. You could also write a function that takes in two objects and compare them.

string firstName = "Jason";
string lastName = "Williams";

firstName.CompareTo(lastName);
I am trying to learn how to use C# generics;
Noah
+5  A: 

You cannot call members on values of generic type parameters unless you have type constraints that definitely require that any type passed as a type parameter would have such members. This is done by constraining them to a specific interface (you cannot just say, "I want any A so long as it has Length).

In your case, there's no good way to do this, because there's no common interface between string and array that would provide Length (or Count, or anything similar). Arrays implement IList<T>, but unfortunately string does not. Both implement IEnumerable<T>, but this doesn't have any way to quickly retrieve the count (you can use Enumerable.Count, but it will actually iterate the whole string to compute the number of characters).

Pavel Minaev
A: 

I don't think generics is a good fit here.

To fix your errors, you would need to constrain A (using generic type constraints) to an interface which has an indexer property and a Length property. However, that's not really what you want to do.

What's wrong with method overloading, as per your current implementation?

Winston Smith
Nothing wrong with method overloading, but my long term goal will involve a much more complex routine. I am trying to reduce the maintaince issue
Noah
Side question, what interface has either the Indexer or Length properties?
Noah
You'd have to create one and wrap your types in that - as I said, not something that you'd want to do.
Winston Smith
+2  A: 

I would certainly classify this as weird code, but by looking at the types you are using you can find some commonalities:

public static IEnumerable<int> Do<T1,T2>(IEnumerable<T1> one, IEnumerable<T2> two) 
    where T1 : IComparable where T2 : IComparable 
{
    var arr1 = one.ToArray();
    var arr2 = two.ToArray();
    for (int i = 0; i < arr1.Length; i++)
     yield return arr1[i].CompareTo(arr2[i]);

}

a char and a byte are comparable. This method now returns an iterator that would give you the result of each comparation of a char to a char or a char to a byte on the same index. This method certainly works with other types, provided they can be enumerated and their elements allow to be compared.

flq
+5  A: 

Your ideal solution would have a signature of:

int memcmp<T>( T a, T b ) where T : IHasLengthAndIndexer

where IHasLengthAndIndexer is defined as:

public interface IHasLengthAndIndexer
{
 int Length { get; }
 byte this[ int index ] { get; }
}

Then you could pass in any type assuming the type implements IHasLengthAndIndexer

However you cannot change the interfaces of byte[] and string so this solution would not replace your four methods.

You could, however, create your own class that has implicit operators from byte[] and string and create a method that compares instances of that class, but that isn't using generics.

Tinister
I would add for a reference: http://msdn.microsoft.com/en-us/library/ms379564%28VS.80%29.aspx which gives an introduction to C# generics
Nathan
If this were lolcode, I would suggest renaming "IHasLengthAndIndexer" to "ICanHasLengthAndIndexer". ;)
Sarah Vessels
+1  A: 

This is another instance of "generics are not templates" -- Generics rely on polymorphism defined by type constraints (explicit or implicit), whereas C++ templates are more like sophisticated macros, which let you get by eith duck typing or "static polymorphism".

The suggested int memcmp<A, B>( A a, B b) is something that would work in C++, so long as the types exposed operator[](int) and had a Length property (well, actually to have properties it would have to be C++/CLI...), because there the substitution is done at compile time.

In C#, by contrast, we just know that the arguments are objects, not necessarily of the same type (there being no type constraint) -- so we can only use methods of object.

You could write specialised conversions for byte[] and string to something like Tinster's IHasLengthAndIndexer and have an int memcmp(IHasLengthAndIndexer a IHasLengthAndIndexer b), but that wouldn't need any generics.

From libraries, the most commonly used generics are things like strongly typed collections (the important thing being that the objects share a type, rather than what the type does); and function types (delegates, event handlers and such). I've used them in addition to abstract out behaviour into a base library abstract type so that the concrete type in a consuming assembly can inject its own type information into the shared base -- a trivial example being like class A : Factory<A> where A : new()

Steve Gilham
+1  A: 

Generic parameters without constraints are used as if they were of type System.Object, that's why the compiler argues about using a.Length.

To get type specific syntax available on a generic parameter type you need to add constraints, e.g.:

public void Count<T>(T list) where T : IList

String and byte[] have only two seemingly common interfaces, IEnumerable and IEnumerable<T>, so it is the only usable thing here. However, string is IEnumerable<char> and byte[] is IEnumerable<byte>, so we need to fallback to IEnumerable:

public int MemCmp<TEn>(TEn obj1, TEn obj2) where TEn : IEnumerable 
{
    IEnumerator enumerator1 = obj1.GetEnumerator();
    IEnumerator enumerator2 = obj2.GetEnumerator();

    bool has1Next;
    bool has2Next;

    do
    {
        has1Next = enumerator1.MoveNext();
        has2Next = enumerator2.MoveNext();

        if(has1Next && has2Next)
        {
            if (enumerator1.Current is IComparable)
            {
                int comparison = ((IComparable) enumerator1.Current).CompareTo(enumerator2.Current);

                if (comparison != 0) return (comparison > 0) ? 1 : -1;
            }
            else if (enumerator2.Current is IComparable)
            {
                int comparison = ((IComparable) enumerator2.Current).CompareTo(enumerator1.Current);

                if (comparison != 0) return (comparison > 0) ? -1 : 1;
            }
            else
            {
                throw new ArgumentException("Not comparable types");
            }
        }
        else 
        {
            if(has1Next)
            {
                return 1;
            }
            else
            {
                return -1;
            }
        }
    } while (true);
}

Usage:

MemCmp<IEnumerable>(a, b);

This is one possible solution to implementing your method.

But, due to inherent type incomatibility here, and the static compilation of generics, we have lost strong typing. So, the answer to the question is yes, but at a considerable cost. Even chars and bytes will get boxed when cast to IComparable.

Note also that in your code

if ( a[i] != b[i] ) ...

works because of built-in implicit conversions between byte and char, which cannot be mimicked with generics.

kek444