32
\$\begingroup\$

This isn't urgent, it is more along the lines of trivia or a challenge. The solution works fine as it is, but I suspect it could be better.

What follows is a method I came up with a while back in a rather ugly situation where I need to make a "best effort" to try casting an object of an unrestricted unknown type TO an unrestricted unknown type. The code has just been bugging me. It seems that there should be a more elegant way to do this, but I wanted to get that "Best Effort" part right.

The methods follows the "Try" convention. It accepts an object "value" and an out param of type T, "result." It attempts to cast value into result as type T. If it succeeds, it returns true. If it cannot, it sets result = default(T) and returns false.

It feels like I'm going to lots of trouble in the method. I'd be open to suggestions to streamline this a bit. Comments included here are mostly not in the original... just to explain why a few things were done the way they were done.

/// <summary>
///   Tries to cast <paramref name="value" /> to an instance of type <typeparamref name="T" /> .
/// </summary>
/// <typeparam name="T"> The type of the instance to return. </typeparam>
/// <param name="value"> The value to cast. </param>
/// <param name="result"> When this method returns true, contains <paramref name="value" /> cast as an instance of <typeparamref
///    name="T" /> . When the method returns false, contains default(T). </param>
/// <returns> True if <paramref name="value" /> is an instance of type <typeparamref name="T" /> ; otherwise, false. </returns>
public static bool TryCast<T>(this object value, out T result)
{
    var destinationType = typeof(T);
    var inputIsNull = (value == null || value == DBNull.Value);

    /*
     * If the given value is null, we'd normally set result to null and be done with it.
     * HOWEVER, if T is not a nullable type, then we can't REALLY cast null to that type, so
     * TryCast should return false.
     */
    if (inputIsNull)
    {
        // If T is nullable, this will result in a null value in result.
        // Otherwise this will result in a default instance in result.
        result = default(T);

        // If the input is null and T is nullable, we report success.  Otherwise we report failure.
        return destinationType.IsNullable();
    }

    // Convert.ChangeType fails when the destination type is nullable.  If T is nullable we use the underlying type.
    var underlyingType = Nullable.GetUnderlyingType(destinationType) ?? destinationType;

    try
    {
        /*
         * At the moment I cannot remember why I handled Guid as a separate case, but
         * I must have been having problems with it at the time or I'd not have bothered.
         */
        if (underlyingType == typeof(Guid))
        {
            if (value is string)
            {
                value = new Guid(value as string);
            }
            if (value is byte[])
            {
                value = new Guid(value as byte[]);
            }

            result = (T)Convert.ChangeType(value, underlyingType);
            return true;
        }

        result = (T)Convert.ChangeType(value, underlyingType);
        return true;
    }
    catch (Exception ex)
    {
        // This was originally used to help me figure out why some types weren't casting in Convert.ChangeType.
        // It could be removed, but you never know, somebody might comment on a better way to do THAT to.
        var traceMessage = ex is InvalidCastException || ex is FormatException || ex is OverflowException
                                ? string.Format("The given value {0} could not be cast as Type {1}.", value, underlyingType.FullName)
                                : ex.Message;
        Trace.WriteLine(traceMessage);

        result = default(T);
        return false;
    }
}
\$\endgroup\$
2
  • 3
    \$\begingroup\$ If you think some of the things in your code need explaining, why didn't you include those comments in your original code? If you expect that we would be confused by your code, why do you think your coworkers wouldn't? \$\endgroup\$
    – svick
    Commented Oct 27, 2012 at 20:01
  • \$\begingroup\$ Just for comparison, check out the general conversion extensions that I wrote. We have tests for these and it seems to work very well: goo.gl/CoE8Xx There's no Try version because we have a generalized Attempt mechanism: goo.gl/DfWwYe \$\endgroup\$ Commented Oct 17, 2014 at 18:44

3 Answers 3

30
\$\begingroup\$

For casting, I have a much simpler method in mind:

public static bool TryCast<T>(this object obj, out T result)
{
    if (obj is T)
    {
        result = (T)obj;
        return true;
    }

    result = default(T);
    return false;
}

You don't need to detect nullable types manually since is operator already checks for it: 5 is int? returns true, so the following code writes 5 to the console.

int value = 5;
int? result;
if (value.TryCast(out result))
    Console.WriteLine(result);

The following writes nothing because TryCast returns false.

string value = "5";
int? test;
if (value.TryCast(out test))
    Console.WriteLine(test);

Lastly, the following should write two lines that are "test 1" and "test 2".

var list = new List<string>();
list.Add("test 1");
list.Add("test 2");

IEnumerable<string> enumerable;
if (list.TryCast(out enumerable))
    foreach (var item in enumerable)
        Console.WriteLine(item);

I'm really against this approach:

if (underlyingType == typeof(Guid))
{
    if (value is string)
    {
        value = new Guid(value as string);
    }
    else if (value is byte[])
    {
        value = new Guid(value as byte[]);
    }

    //...

If you want this kind of custom conversion features, my suggestion would be to keep your converters in a static, thread-safe Converter collection. A sample converter class can be like this:

public abstract class Converter
{
    private readonly Type from; // Type of the instance to convert.
    private readonly Type to;   // Type that the instance will be converted to.

    // Internal, because we'll provide the only implementation...
    // ...that's also why we don't check if the arguments are null.
    internal Converter(Type from, Type to)
    {
        this.from = from;
        this.to = to;
    }

    public Type From { get { return this.from; } }
    public Type To { get { return this.to; } }

    public abstract object Convert(object obj);
}

And the implementation is:

// Sealed, because this is meant to be the only implementation.
public sealed class Converter<TFrom, TTo> : Converter
{
    Func<TFrom, TTo> converter; // Converter is strongly typed.

    public Converter(Func<TFrom, TTo> converter)
        : base(typeof(TFrom), typeof(TTo)) // Can't send null types to the base.
    {
        if (converter == null)
            throw new ArgumentNullException("converter", "Converter must not be null.");

        this.converter = converter;
    }

    public override object Convert(object obj)
    {
        if (!(obj is TFrom))
        {
            var msg = string.Format("Object is not of the type {0}.", this.From.FullName);
            throw new ArgumentException(msg, "obj");
        }

        // Can throw exception, it's ok.
        return this.converter.Invoke((TFrom)obj);
    }
}

To initialize it:

var int32ToString = new Converter<int, string>(i => i.ToString());
var stringToInt32 = new Converter<string, int>(s => int.Parse(s));

// Converters should be a thread-safe collection of our abstract `Converter` type.
Converters.Add(int32ToString);
Converters.Add(stringToInt32);

With support for the custom converters, the final TryCast method becomes this:

public static bool TryCast<T>(this object obj, out T result)
{
    if (obj is T)
    {
        result = (T)obj;
        return true;
    }

    // If it's null, we can't get the type.
    if (obj != null)
    {
        var converter = Converters.FirstOrDefault(c =>
            c.From == obj.GetType() && c.To == typeof(T));

        // Use the converter if there is one.
        if (converter != null)
            try
            {
                result = (T)converter.Convert(obj);
                return true;
            }
            catch (Exception)
            {
                // Ignore - "Try*" methods don't throw exceptions.
            }
    }

    result = default(T);
    return false;
}
\$\endgroup\$
3
  • \$\begingroup\$ You can use the "as" keyword to simplify your TryCast<T> implementation to just two lines. bool TryCast<T>(this object obj, out T result) { result = obj as T; return result != default(T); } \$\endgroup\$ Commented Jan 25, 2016 at 18:16
  • 4
    \$\begingroup\$ @user2023861 you can't use as unless where T : class if I remember correctly. \$\endgroup\$ Commented Jan 28, 2016 at 9:46
  • \$\begingroup\$ @user2023861 additionally, result being equal to default value is not a reliable indication, that cast has failed. \$\endgroup\$
    – Nikita B
    Commented Jun 15, 2018 at 9:42
9
\$\begingroup\$

There are built-in converters in System.ComponentModel.TypeDescriptor namespace (System.dll). You don't have to write your own array and you have access to a bunch of pre-existing converters as well.

This is a modified version to account for this:

public static bool TryCast<T>(object obj, out T result)
{
    result = default(T);
    if (obj is T)
    {
        result = (T)obj;
        return true;
    }

    // If it's null, we can't get the type.
    if (obj != null)
    {
        var converter = TypeDescriptor.GetConverter(typeof (T));
        if(converter.CanConvertFrom(obj.GetType()))
            result = (T) converter.ConvertFrom(obj);
        else
            return false;

        return true;
    }

    //Be permissive if the object was null and the target is a ref-type
    return !typeof(T).IsValueType; 
}

(of course the permissiveness depends on how you plan yo use it, I rather not have a try around the conversion, since I check for it)

\$\endgroup\$
4
  • 3
    \$\begingroup\$ Welcome to Code Review! This looks like a valuable answer! Even if your advice was just "There are build in converters in System.ComponentModel.TypeDescriptor namespace (System.dll), you dont have to write your own array and you have access to a bunch of pre-existing converters as well." then that would be a valuable review. Even small pieces of feedback are worth posting as reviews. \$\endgroup\$ Commented Jan 25, 2016 at 17:10
  • \$\begingroup\$ I believe your TryCast method will sometimes throw exceptions. I think an exception will be thrown not for going to Byte from (Int32)1 or (Int32)999 or (String)"1" but will for (String)"999". I don't know if this is a problem for anyone, but perhaps that specific case could be avoided? \$\endgroup\$ Commented Jun 12, 2017 at 10:45
  • \$\begingroup\$ @captainjamie You mean something like TryCast<Byte>("999", out b) ? perhaps the usage of get converter can be improved, feel free to update my question or add the right try-catch Also it's worth noting that you can check converters both ways: TypeConverter targetConverter = TypeDescriptor.GetConverter(typeof(T)); TypeConverter sourceConverter = TypeDescriptor.GetConverter(obj.GetType()); \$\endgroup\$
    – GettnDer
    Commented Jun 13, 2017 at 17:53
  • \$\begingroup\$ @GettnDer TryCast<Byte>("999", out b) is exactly what I mean. And the correct try-catch would be catch(Exception) because the converter throws the base class. It's a difficult problem to fix, there seems to be no good solution. \$\endgroup\$ Commented Jun 14, 2017 at 10:39
1
\$\begingroup\$

I used GettnDer's answer and updated it to make use of the c# 8 language features eliminating some of the need for casting as well as show how one can enable "123" to be parsed and test for type, same goes for IP addresses etc, lots of things support parsing. I have an interface that denotes something implements IParse so I test if

if(T is IParse parse)
{
 return parse.TryParse<T>(obj.ToString(),out result );
}

There are lots of "helpers" you can think of... The tryCast method that I think solves the question and most comments using the new features efficiently could be like:

public static bool TryCast<T>(object obj, out T result)
{
    //will cover 99%
    if (obj is T value)
    {
        result = value;
        return true;
    }

    // will cover most plausible data type
    if (obj is not null)
    {
        var str = obj.ToString();
        switch (true)
        {
            case var _ when typeof(T) == typeof(string):
                result = (T)obj;
                return true;
            case var _ when typeof(T) == typeof(int) && int.TryParse(str, out var aValue):
                result = (T)(object)aValue;
                return true;
            case var _ when typeof(T) == typeof(long) && long.TryParse(str, out var aValue):
                result = (T)(object)aValue;
                return true;
            case var _ when typeof(T) == typeof(DateTime) && DateTime.TryParse(str, out var aValue):
                result = (T)(object)aValue;
                return true;
            case var _ when typeof(T) == typeof(byte) && byte.TryParse(str, out var aValue):
                result = (T)(object)aValue;
                return true;
            case var _ when typeof(T) == typeof(short) && short.TryParse(str, out var aValue):
                result = (T)(object)aValue;
                return true;
        }

    }

This will work in all frameworks that support c# 8 so .net core 3.1 Net 5.0 and .net standard 2.1

\$\endgroup\$
0

Not the answer you're looking for? Browse other questions tagged or ask your own question.