1
\$\begingroup\$

I often need to update properties using the out parameter of the standard TryParse methods. However, use of a property as out or ref parameter is not allowed. So, I wrote my own implementation of the TryParse method MyTryParse using the generic extension method, which is supposed to work for all standard value types.

Codes:

class Program
{
    static void Main(string[] args)
    {
        new Tester().TestIt();
        Console.Read();
    }
}

class Tester
{
    public bool BooleanP { get; set; } = true;
    public char CharP { get; set; } = 'A';
    public int IntP { get; set; } = 123;
    public double DoubleP { get; set; } = 3.14;
    public DateTime DateTimeP { get; set; } = DateTime.Now;

    public struct TestStruct
    {
        public int X { get; set; }
    };
    public TestStruct StructP { get; set; }

    public void TestIt()
    {
        try
        {
            // bool.TryParse("TRUE", out BooleanP); // ERROR: A property or indexer may not be passed as out or ref parameter

            // bool
            BooleanP = BooleanP.MyTryParse("bad");
            BooleanP.PPrint("\nBooleanP");

            BooleanP = BooleanP.MyTryParse("FALSE");
            BooleanP.PPrint("BooleanP");

            // char
            CharP = CharP.MyTryParse("xx");
            CharP.PPrint("\nCharP");

            CharP = CharP.MyTryParse("Y");
            CharP.PPrint("CharP");

            // int
            IntP = IntP.MyTryParse("0.1");
            IntP.PPrint("\nIntP");

            IntP = IntP.MyTryParse("555");
            IntP.PPrint("IntP");

            // double
            DoubleP = DoubleP.MyTryParse("3xxx");
            DoubleP.PPrint("\nDoubleP");

            DoubleP = DoubleP.MyTryParse("3.1415");
            DoubleP.PPrint("DoubleP");

            // DateTime
            DateTimeP = DateTimeP.MyTryParse("20180707");
            DateTimeP.PPrint("\nDateTimeP");

            DateTimeP = DateTimeP.MyTryParse("2018.07.07");
            DateTimeP.PPrint("DateTimeP");

            // struct
            Console.WriteLine();
            StructP = StructP.MyTryParse("9", true);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

public static class Extensions
{
    private enum StandardTtype
    {
        Undefined,
        Boolean,
        Byte, SByte,
        Char,
        Int16, UInt16, Int32, UInt32, Int64, UInt64,
        Single, Double, Decimal, DateTime
    };

    public static T MyTryParse<T>(this T value, string text, bool throwEx = false) where T : struct
    {
        object oVal = value;
        string typeName = typeof(T).Name;
        StandardTtype type = StandardTtype.Undefined;
        Enum.TryParse(typeName, out type);
        switch (type)
        {
            case StandardTtype.Boolean:
                {
                    if (bool.TryParse(text, out bool outVal))
                        oVal = outVal;
                    else ThrowException();
                }
                break;
            case StandardTtype.Byte:
                {
                    if (byte.TryParse(text, out byte outVal))
                        oVal = outVal;
                    else ThrowException();
                }
                break;
            case StandardTtype.SByte:
                {
                    if (sbyte.TryParse(text, out sbyte outVal))
                        oVal = outVal;
                    else ThrowException();
                }
                break;
            case StandardTtype.Char:
                {
                    if (char.TryParse(text, out char outVal))
                        oVal = outVal;
                    else ThrowException();
                }
                break;
            case StandardTtype.Int16:
                {
                    if (short.TryParse(text, out short outVal))
                        oVal = outVal;
                    else ThrowException();
                }
                break;
            case StandardTtype.UInt16:
                {
                    if (ushort.TryParse(text, out ushort outVal))
                        oVal = outVal;
                    else ThrowException();
                }
                break;
            case StandardTtype.Int32:
                {
                    if (int.TryParse(text, out int outVal))
                        oVal = outVal;
                    else ThrowException();
                }
                break;
            case StandardTtype.UInt32:
                {
                    if (uint.TryParse(text, out uint outVal))
                        oVal = outVal;
                    else ThrowException();
                }
                break;
            case StandardTtype.Int64:
                {
                    if (long.TryParse(text, out long outVal))
                        oVal = outVal;
                    else ThrowException();
                }
                break;
            case StandardTtype.UInt64:
                {
                    if (ulong.TryParse(text, out ulong outVal))
                        oVal = outVal;
                    else ThrowException();
                }
                break;
            case StandardTtype.Single:
                {
                    if (float.TryParse(text, out float outVal))
                        oVal = outVal;
                    else ThrowException();
                }
                break;
            case StandardTtype.Double:
                {
                    if (double.TryParse(text, out double outVal))
                        oVal = outVal;
                    else ThrowException();
                }
                break;
            case StandardTtype.Decimal:
                {
                    if (decimal.TryParse(text, out decimal outVal))
                        oVal = outVal;
                    else ThrowException();
                }
                break;
            case StandardTtype.DateTime:
                {
                    if (DateTime.TryParse(text, out DateTime outVal))
                        oVal = outVal;
                    else ThrowException();
                }
                break;
            case StandardTtype.Undefined:
            default:
                if (throwEx)
                    throw new Exception($"The type '{typeName}' is unhandled");
                break;
        }

        try
        {
            value = (T)oVal;
        }
        catch (Exception ex)
        {
            if (throwEx)
                throw new Exception($"Cannot convert '{oVal.ToString()}' into '{typeName}'", ex);
        }

        void ThrowException()
        {
            if (throwEx)
                throw new Exception($"Cannot parse \"{text}\" into `{typeName}`");
        }

        return value;
    }

    public static void PPrint<T>(this T value, string name = "value")
    {
        Console.WriteLine(name + " = " + value );
    }
}

The main purpose of this method is to work quietly (unless I tell to do otherwise) and update properties (or fields) if this is possible.

Question: Is something in the MyTryParse method which can go wrong?

Any other suggestion for improving performance, style of readability would be very welcome.

\$\endgroup\$
8
  • 3
    \$\begingroup\$ Could you post an example of your real problem and explain why this is a problem, I mean a case of: I often need to update properties using the out parameter of the standard because this code it one of the most unconventional and confusing ways of solving something that probalby should be solved in a completely different way. To me it's a clear case of the XY Problem. \$\endgroup\$
    – t3chb0t
    Jul 4, 2018 at 15:23
  • \$\begingroup\$ @t3chb0t I didn't knew about the XY problem. Thanks for that.. my problem is that: bool.TryParse("TRUE", out BooleanP); is not allowed, where BooleanP is the property. I have it commented in my TestIt() method. I am trying to update a property from a string value with a single simple command like the bool.Parse("TRUE");. Except the last one throws an exception if it is not successful. \$\endgroup\$
    – L_J
    Jul 4, 2018 at 19:46
  • \$\begingroup\$ The question is still why are you doing this? Why do you want to use a property there? This is pretty unusual and this is your problem Y that's why I'm asking ;-) you are not telling us about the actual issue, your problem X. \$\endgroup\$
    – t3chb0t
    Jul 4, 2018 at 19:51
  • 1
    \$\begingroup\$ Because in my WinForms applications when a user changes some control's text to update the corresponding auto property. if parsing is possible. \$\endgroup\$
    – L_J
    Jul 4, 2018 at 19:58
  • 2
    \$\begingroup\$ @L_J: if processing user input is the main use-case for this code, then I guess you're not familiar with data binding? \$\endgroup\$ Jul 5, 2018 at 8:16

2 Answers 2

2
\$\begingroup\$
  • Trying to put all this type-specific code into a single 'generic' method requires runtime type checks and boxing/unboxing. You're paying for this at runtime, but what do you gain from it? Writing several type-specific methods seems more appropriate here. You would have to write multiple overloads anyway if you want to provide additional parsing settings (such as number styles or exact date formats).
  • Making this an extension method is confusing: this T value is actually used as a fallback value for when exception throwing is disabled, it's not really 'operating' on the given value. ID = ID.TryParse(input); is short, but it's not very clear what's going on - why are you calling a parse method on something that you're also assigning to? I'd probably go for something like ID = Util.TryParse(input, fallbackValue: ID); - more writing, but also easier to understand.
  • If you must perform runtime type checks, then don't use Type.Name: that will fail for user-made types with perfectly valid names like Boolean or Int32. Type.FullName should be more reliable, but why not just compare against typeof(bool) and the like, or use Type.GetTypeCode(t), which gives you a TypeCode enum?

You've essentially written a method that can either return a fallback value or throw an exception on failure - it's Parse and TryParse in one:

public static bool TryParse(string input, bool fallbackValue = false, bool throwOnFailure = false)
{
    if (throwOnFailure)
        return bool.Parse(input);
    else
        return bool.TryParse(input, out var result) ? result : fallbackValue;
}

// repeat for other (commonly used) types
\$\endgroup\$
0
\$\begingroup\$

I must admit, that I hardly can imagine where to use a default value (the current value of a property) if a conversion of string input fails? When say IntP.TryParse("0.1") fails with throwEx = false you'll get back IntP = 123 - without any warning and therefore any knowledge of the error. This can lead to "fatalities" of any kind in a program and no one will know. You should not do that. If a conversion fails you should certainly inform the user or log the failure in some way.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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