Click here to Skip to main content
Email Password   helpLost your password?

Background

Occasionally we find ourselves in a situation where we have an object, and we want to determine which of several types of object it is in order to do something with it. For instance, we may have a Control, and if it's a Button we may want to do one thing with it, but if it's a TextBox we may want to do something else with it.

A fast, simple, and reliable way of handling that is:

void F ( object item )
{
    if ( item is Button ) ...
    else if ( item is TextBox ) ...
}

Of great importance here is that this technique handles derived types properly. (It also works with interfaces, the other techniques presented here don't.)

This works very well, but can become unwieldy as more and more tests are added, both in terms of maintenance and performance.

Obviously, a switch would help smooth things out, but we can't use the type directly. The following won't compile:

void F ( object item )
{
    switch ( item.GetType() )
    {
        case Button  : ...
        case TextBox : ...
    }
}

We can use strings with switch statements, so two common techniques are:

void F ( object item )
{
    switch ( item.GetType().Name )
    {
        case "Button"  : ...
        case "TextBox" : ...
    }
}

and

void F ( object item )
{
    switch ( item.GetType().FullName )
    {
        case "System.Windows.Forms.Button"  : ...
        case "System.Windows.Forms.TextBox" : ...
    }
}

There are also some among us who argue against using strings in switch statements; for them I submit the following:

enum Controls 
{ 
    Button 
, 
    TextBox 
... 
}
 
void F ( object item )
{
    if ( Enum.IsDefined ( typeof(Controls) , item.GetType().Name ) )
    {
        switch ( (Controls) Enum.Parse ( typeof(Controls) , item.GetType().Name ) )
        {
            case Controls.Button  : ...
            case Controls.TextBox : ...
        }
    }
}

None of these techniques will handle derived types properly:
When passed an item of a type that derives from Button, but isn't named "Button" it won't pass the test though it probably should (false-negative).

In the first and third snippets, when passed an item of a type that doesn't derive from Button, but is named "Button" it will pass the test and probably cause an InvalidCastException (false-positive).

Therefore, while these techniques may be "good enough" in many cases, they are not completely reliable, so I feel that they are not suitable as general solutions.

Using an EnumTransmogrifier

The idea of using of an enumeration and Enum.Parse is actually pretty good, but it's subject to false-positives because we're limited to using the Name of the type rather than the Fullname. (Because the FullName contains characters that aren't allowed in a simple name.)

Mapping strings to enumeration values is what my EnumTransmogrifier [^] is all about:

enum Controls 
{ 
    [Description("System.Windows.Forms.Button")]
    Button 
, 
    [Description("System.Windows.Forms.TextBox")]
    TextBox 
... 
}
 
EnumTransmogrifier<Controls> controls = new EnumTransmogrifier<Controls>() ;
 
void F ( object item )
{
    Controls control ;
 
    if ( controls.TryParse ( item.GetType().FullName , out control ) )
    {
        switch ( control )
        {
            case Controls.Button  : ...
            case Controls.TextBox : ...
        }
    }
}

That takes care of the false-positives. For the false-negatives we can check the type's BaseType, looping until we either find a match or run out of types to try.

TypeTransmogrifier

A TypeTransmogrifier is similar to an EnumTransmogrifier but it maps enumeration values to types rather than strings:

Update 2008-06-12: The class is now static and does not wrap an EnumTransmogrifier.

public static partial class TypeTransmogrifier<T>
{
    private static class Null{}
    private static System.Type Nullity = typeof(Null) ;
 
    public static readonly System.Type BaseType = typeof(T) ;
 
    public static T DefaultValue ;

    private readonly System.Collections.Generic.Dictionary<System.Type,T> types =
        new System.Collections.Generic.Dictionary<System.Type,T>() ;
 
    private readonly System.Collections.Generic.Dictionary<T,System.Type> values =
        new System.Collections.Generic.Dictionary<T,System.Type>() ;
}

The Null class is used because null is not allowed as a key in a Dictionary.

The constructor iterates the values of the enumeration, accesses the attributes and populates the Dictionaries.

static TypeTransmogrifier
(
) 
{
    if ( !BaseType.IsEnum )
    {
        throw ( new System.ArgumentException ( "T must be an Enum" ) ) ;
    }

    PIEBALD.Attributes.EnumDefaultValueAttribute.GetDefaultValue<T> ( out DefaultValue ) ;

    System.Type atttyp = typeof(PIEBALD.Attributes.TypeTransmogrifierAttribute) ;
        
    System.Type temp ;

    foreach 
    ( 
        System.Reflection.FieldInfo field 
    in 
        BaseType.GetFields
        ( 
            System.Reflection.BindingFlags.Public 
        | 
            System.Reflection.BindingFlags.Static 
        )
    )
    {
        if ( field.FieldType == BaseType )
        {
            foreach 
            ( 
                PIEBALD.Attributes.TypeTransmogrifierAttribute att 
            in 
                field.GetCustomAttributes ( atttyp , false )
            )
            {
                if ( att.Type == null )
                {
                    temp = Nullity ;
                }
                else
                {
                    temp = att.Type ;
                }
                
                if ( types.ContainsKey ( temp ) )
                {
                    throw ( new System.ArgumentException 
                    ( 
                        "Not all the types are unique." 
                    ,
                        field.Name
                    ) ) ;
                }
                
                types [ temp ] = (T) field.GetValue ( null ) ;
                values [ types [ temp ] ] = temp ;
            }
        }
    }
    
    return ;
}

The TryParse method uses the Dictionary of types and also checks the item's BaseType as needed:

public static bool
TryParse
(
    object Subject
,
    out T  Result
)
{
    bool result = false ;
    
    Result = DefaultValue ;
    
    if ( Subject == null )
    {
        if ( result = types.ContainsKey ( Nullity ) )
        {
            Result = types [ Nullity ] ;
        }
    }
    else
    {
        System.Type objtype ;
        
        if ( Subject is System.Type )
        {
            objtype = (System.Type) Subject ;
        }
        else
        {
            objtype = Subject.GetType() ;
        }
    
        while ( !result && ( objtype != null ) )
        {
            if ( result = types.ContainsKey ( objtype ) )
            {
                Result = types [ objtype ] ;
            }
            else
            {
                objtype = objtype.BaseType ;
            }
        }
    }
        
    return ( result ) ;
}

Note that null references will result in a match if the enumeration is set up to allow that. Also, when a System.Type is passed in it will be used directly.

Update 2008-06-12: Added the following two methods.

Call TryParse and throw ArgumentException if it fails.

public static T
Parse
(
    object Subject
)
{
    T result = DefaultValue ;
    
    if ( !TryParse ( Subject , out result ) )
    {
        string typ ;
        
        if ( Subject == null )
        {
            typ = "null" ;
        }
        else
        {
            if ( Subject is System.Type )
            {
                typ = ((System.Type) Subject).FullName ;
            }
            else
            {
                typ = Subject.GetType().FullName ;
            }
        }

        throw ( new System.ArgumentException
        (
            "The supplied type did not translate."
        ,
            typ
        ) ) ;
    }            
    
    return ( result ) ; 
}

Attempt to get the Type associated with the enumeration value.

public static System.Type
GetType
(
    T Value
)
{
    System.Type result = null ;
    
    if ( values.ContainsKey ( Value ) )
    {
        if ( values [ Value ] != Nullity )
        {
            result = values [ Value ] ;
        }
    }
    else
    {
        throw ( new System.ArgumentException
        (
            "The supplied value did not translate."
        ,
            "Value"
        ) ) ;
    }            
    
    return ( result ) ;
}

Using the Code

In the earlier snippet, replace the EnumTransmogrifier with a TypeTransmogrifier and pass the item directly to TryParse:

enum Controls 
{ 
    [Description("System.Windows.Forms.Button")]
    Button 
, 
    [Description("System.Windows.Forms.TextBox")]
    TextBox 
... 
}
 
// EnumTransmogrifier<Controls> controls = new EnumTransmogrifier<Controls>() ;
TypeTransmogrifier<Controls> controls = new TypeTransmogrifier<Controls>() ;
 
void F ( object item )
{
    Controls control ;
 
//    if ( controls.TryParse ( item.GetType().FullName , out control ) )
    if ( controls.TryParse ( item , out control ) )
    {
        switch ( control )
        {
            case Controls.Button  : ...
            case Controls.TextBox : ...
        }
    }
}

TypeDemo.cs

The included TypeDemo.cs file uses the following enumeration:

public enum WinForms
{
    [PIEBALD.Attributes.TypeTransmogrifierAttribute(null)]
    Null
,
    [PIEBALD.Attributes.TypeTransmogrifierAttribute(typeof(System.Windows.Forms.Control))]
    Control
,
    [PIEBALD.Attributes.TypeTransmogrifierAttribute(typeof(System.Windows.Forms.ButtonBase))]
    ButtonBase
,
    [PIEBALD.Attributes.TypeTransmogrifierAttribute(typeof(System.Windows.Forms.Button))]
    Button
} ;

And defines the following classes which could cause false-positives and false-negatives with other techniques:

private class Button {}
private class MyButton : System.Windows.Forms.Button {}

The demo will simply show the results of the TryParse:

private static void
Show
(
    object What
)
{
    bool     res ;
    WinForms typ ;
 
    res = PIEBALD.Types.TypeTransmogrifier<WinForms>.TryParse ( What , out typ ) ;
 
    System.Console.WriteLine ( "{0} {1}" , res , typ ) ;
 
    return ;
}

Show ( null ) ;                                   // True  Null
Show ( new System.DateTime() ) ;                  // False Null
Show ( new System.Windows.Forms.Button() ) ;      // True  Button
Show ( new System.Windows.Forms.RadioButton() ) ; // True  ButtonBase  -- not a false-negative
Show ( new System.Windows.Forms.TextBox() ) ;     // True  Control     -- not a false-negative
Show ( new System.Web.UI.WebControls.Button() ) ; // False Null        -- not a false-positive
Show ( new Button() ) ;                           // False Null        -- not a false-positive
Show ( new MyButton() ) ;                         // True  Button      -- not a false-negative
Show ( typeof(MyButton) ) ;                       // True  Button      -- not a false-negative
Show ( (new MyButton()).GetType() ) ;             // True  Button      -- not a false-negative

History

2008-06-10 First submitted

2008-06-12 Made it static, added use of TypeTransmogrifierAttribute (at leppie's suggestion), added Parse and GetType

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
General'if' is slower than reflection?
alnicol
5:11 12 Jun '08  
Have you done any performance testing on this? The performance cost reflecting over your types must be much higher than any performance hit from using 'if' statements (as opposed to 'switch'). In fact, from what I have read, 'if' statements and 'switch' statements are almost identical performance under the CLR.

Surely the issue here is design. Why would you ever have a 'switch' statement or 'if' statment with so many options that it becomes unwiedly and slow? If it got to this stage, I think I'd go back and investigate some OO design principles/patterns.

Still, it's certainly an 'interesting' way of doing things ...
GeneralRe: 'if' is slower than reflection?
leppie
6:38 12 Jun '08  
I do not see where he is using reflection (besides getting the type of an object).

xacc.ide - now with TabsToSpaces support
IronScheme - 1.0 alpha 4a out now (29 May 2008)

GeneralRe: 'if' is slower than reflection?
alnicol
10:00 12 Jun '08  
Yup, that's exactly the point. Calling GetType() to get the type of an object is slower than doing an 'is' ... a quick test I wrote suggests it takes twice as long. I know we're not talking much time, but I wonder whether the speed loss due to GetType outweighs the speed increase from using a dictionary lookup rather than a 'if'. I suspect there's not much in it, which makes me wonder whether the added complexity of using this method is a bit pointless.
GeneralRe: 'if' is slower than reflection?
PIEBALDconsult
10:48 12 Jun '08  
The if and is technique is quickest and has other benefits, but it can become unwieldy.

The switch techniques are slower and have other limitations, but a switch
can be more flexible.

My intent was to implement a switch that doesn't suffer the limitations of the ones I'd tried before.
GeneralRe: 'if' is slower than reflection?
alnicol
10:53 12 Jun '08  
Anyway, minimal speed differences aside, it was an interesting article and certainly made me think for a bit. Plus, I like the name Transmogrifier ... will have to try and work that into my code at some point! Smile
GeneralRe: 'if' is slower than reflection?
PIEBALDconsult
11:39 12 Jun '08  
alnicol wrote:
minimal speed differences


Not so minimal after all; after 100,000,000 passes:

if/is ==> 605 milliseconds
TypeTransmogrifier ==> 21231 milliseconds


So if one of my Services processes one million records a day (and none are near that) it will waste twenty-one seconds every hundred days? I can live with that. Big Grin
GeneralRe: 'if' is slower than reflection?
PIEBALDconsult
8:58 12 Jun '08  
alnicol wrote:
The performance cost reflecting over your types


Any cost there is only incured once, at construction time, after that it's just accessing a Dictionary.


alnicol wrote:
Why would you ever have a 'switch' statement or 'if' statment with so many options


In particular, when I access the UnderlyingType of an enumeration there are eight possibilities.


alnicol wrote:
go back and investigate


Which is what brought this about. Big Grin
Generalis operator
mahkomat
1:24 12 Jun '08  
what about the is operator?

if (untypedObject is Control)
{
}

else if (untypedObject is String)
{
}

since this type reflecting thing is not that fast, it is recommended to put the types with the highes probability first in the if/else-tree.
GeneralRe: is operator
leppie
6:38 12 Jun '08  
mahkomat wrote:
since this type reflecting thing is not that fast


That is not reflect, and it is pretty damn fast if you ask me! Smile

xacc.ide - now with TabsToSpaces support
IronScheme - 1.0 alpha 4a out now (29 May 2008)

GeneralRe: is operator [modified]
PIEBALDconsult
8:49 12 Jun '08  
Right, that's the first technique I presented.


mahkomat wrote:
put the types with the highes probability first


That won't always work; if you test for Control and Button you must test for Button first even though Control may have the higher probability.

modified on Thursday, June 12, 2008 3:16 PM

GeneralTip
leppie
19:46 11 Jun '08  
Instead of using a non typesafe attribute on the enum, using DescriptionAttribute, create your own, taking a type argument instead. Eg:
class TypeAttribute : Attribute
{
Type t;
public TypeAttribute(Type t) { this.t = t; }
}

This will save you from looking for types, and will load types whose assemblies are not already loaded.

Cheers

leppie

xacc.ide - now with TabsToSpaces support
IronScheme - 1.0 alpha 4a out now (29 May 2008)

GeneralRe: Tip [modified]
PIEBALDconsult
11:11 12 Jun '08  
A) I forgot I could.
B) DescriptionAttribute is non typesafe?
C) It allows for null.
D) It means I can have an EnumTransmogrifier as well.

I'll give it some more thought, I may be able to do that.

Thanks.

P.S. Well of course it works.
It allows null, and by having a property to return the FullName it still works with EnumTransmogrifier.
Still some cleanup to do.

modified on Friday, June 13, 2008 1:37 AM

GeneralRe: Tip [modified]
PIEBALDconsult
14:02 13 Jun '08  
Updated


I just realized that I forgot to document the attribute. Red faced

modified on Friday, June 13, 2008 7:27 PM


Last Updated 13 Jun 2008 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010