简体   繁体   中英

Recover or return from a thrown exception

For many years my main language was Perl, and I regularly validated user input without a problem. Now I'm using a lot of C# and want to migrate toward the throw/catch style of validating user input and recovering/returning from thrown exceptions. I'm using a very naive (ie, stupid) method of doing this, and feel an urgent need to move to something a little more mature and less stupid. I have copied a function that returns an integer from a prompt. I'm recovering from user errors by using the dreaded GOTO statement. What is the better way to do this?

Thanks, CC.

private static int GetInput(string v)
{
    begin:
    Console.Write(v);
    string strradius = Console.ReadLine();
    int intradius;
    try
    {
        intradius = int.Parse(strradius);
        if (intradius < 1)
            throw new ArgumentOutOfRangeException();
    }
    catch (ArgumentNullException)
    {
        Console.WriteLine("You must enter a value.");
        goto begin;
    }
    catch (FormatException)
    {
        Console.WriteLine("You must enter a valid number.");
        goto begin;
    }
    catch (ArgumentOutOfRangeException)
    {
        Console.WriteLine("Your number is out of range");
        goto begin;
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
        goto begin;
    }
    finally
    {
        Console.WriteLine("Okay");
    }
    return intradius;
} 

First, a good rule of thumb as to when to use goto is never. Really, other than for a handful of very rare exceptional circumstances, you'd never want to use it.

Next, to your question, using exceptions to validate input is a bad idea in general. As most people pointed out it's expensive. Exceptions should be used to handle exceptional circumstances, so I would in fact not use them at all.

Instead, you can use a do-while loop, and repeat as long as the user inputs an incorrect input. You exit the loop once you get a proper input. If in case an exception occurs, you shouldn't really continue the process. Either handle it outside (ie, no try-catch inside your method) or else if you must do a try-catch then simply print a message and exit the method. But I would not use exception handling for a method like this. Also it's a good idea to actually change the return type to bool, so you indicate to the outside world whether the method succeeded or not by the return type. You an use an out parameter to actually return the converted int .

private static bool GetInput(string msg, out int converted)
{
    bool result = false;
    converted = 0;
    do
    {
        Console.Write(msg);
        string str = Console.ReadLine();
        result = int.TryParse(str, out converted);
        if (result && converted < 1)
        {
            Console.WriteLine("Your number is out of range");
            result = false;
        }
        if (!result && string.IsNullOrEmpty(str))
        {
            Console.WriteLine("You must enter a value.");
        }
        if (!result && !string.IsNullOrEmpty(str))
        {
            Console.WriteLine("You must enter a valid number.");
        }
    } while (!result);

    return result;
}

Using goto statements in C# code is highly frowned-upon because it makes code difficult to read, debug, and maintain (for more info, read this ). Loops, if/then statements, or method calls can be used instead of goto statements. Also, try \\ catch blocks should be used sparingly, to catch exceptions that you are unable to handle.

In your case, we can use a while loop to continue to loop until a valid number is entered, and we can use the int.TryParse method to attempt to parse the string and get an integer result. This method returns a Boolean that indicates success, and takes an out parameter that will be set to the integer result.

My suggestion for your method would be to have it take in a string that will be used as a prompt for the user (asking them to enter a number), and return the integer result of their input.

For example:

private static int GetIntFromUser(string prompt, int minValue = int.MinValue, 
    int maxValue = int.MaxValue)
{           
    int result;
    string errorMsg = $"ERROR: Input must be a valid number from {minValue} to {maxValue}";

    while(true)
    {
        Console.Write(prompt);
        string input = Console.ReadLine();

        if (!int.TryParse(input, out result) || result < minValue || result > maxValue)
        {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine(errorMsg);
            Console.ResetColor();
        }
        else
        {
            break;
        }
    }

    return result;
}

In practice we can now call this method to get numbers from the user and we'll know they are valid without having to do any additional validation:

private static void Main()
{
    // Age must be between 5 and 100
    var age = GetIntFromUser("Please enter your age: ", 5, 100);

    // Weight must be a positive number (minimum of zero)
    var weight = GetIntFromUser("Please enter your weight: ", 0);

    // No restrictions on favorite number
    var favNum = GetIntFromUser("Enter your favorite whole number: ");

    // This is a similar method I wrote to pause the program with a prompt
    GetKeyFromUser("\nDone! Press any key to exit...");
}

Output

![在此处输入图片描述

I'd write it something like this (though I'd probably give the user a chance to give up):

 private static int GetInput(string v)
 {
     int intradius = 0;   //needs to be initialized to keep compiler happy
     while (true)
     {
         Console.Write($"{v}: ");
         string strradius = Console.ReadLine();
         if (!int.TryParse(strradius, out intradius))
         {
             Console.WriteLine($"An integer is required: [{strradius}] is not an integer");
         }
         else if (intradius < 1)
         {
             Console.WriteLine($"The entered number [{intradius}] is out of range, it must be one or greater");
         }
         else
         {
             break;      //breaking out of the while loop, the input is good
         }
     }

     return intradius;
 }

For recoverable validation use conditional code/checks rather than relying on exceptions. Exceptions are expensive from a performance perspective primarily because they will generate a call stack.

Instead, look at something like:

private static int GetInput(string v)
{
    Console.Write(v);
    string strradius = Console.ReadLine();

    if (string.IsNullOrEmpty(strradius)
    {
        Console.WriteLine("You must enter a value.");
        return 0;
    }

    int intradius;

    bool result = int.TryParse(strradius, out intradius);
    if (!result)
        Console.WriteLine("You must enter a valid number.");
    else if (intradius < 1)
        Console.WriteLine("Your number is out of range");

    Console.WriteLine("Okay");
    return intradius;
} 

Personally, I like to wrap business logic results:

// Result container.
private class RadiusValidationResults
{
   public bool IsSuccessful {get; private set;}
   public int Radius {get; private set;}
   public string FailureReason {get; private set;}

   private RadiusValidationResults()
   { }

   public static RadiusValidationResults Success(int result)
   {
      return new RadiusValidationResults { IsSuccessful = true, Radius = result };
    }

    public static RadiusValidationResults Failure(string failureReason)
    {
      return new RadiusValidationResults { FailureReason = failureReason };
    }
}

// Validation business logic.
private static RadiusValidationResult ValidateRadius(string input)
{
    if (string.IsNullOrEmpty(input)
        return RadiusValidationResult.Failure("You must enter a value.");

    int radius;

    if (!int.TryParse(strradius, out radius))
        return RadiusValidationResult.Failure("You must enter a valid number.");
    else if (intradius < 1)
        return RadiusValidationResult.Failure("Your number is out of range");

    return RadiusValidationResult.Success(radius);   
}

then your calling method that interacts with the Console:

private static int GetInput()
{
   try
   {
      var result = ValidateRadius(Console.ReadLine());
      if(!result.IsSuccessful)
         Console.WriteLine(result.FailureReason);
      else
         Console.WriteLine("Okay");

      return result.Radius;
   catch // Here you can handle specific exception types, or bubble to a higher level. Log exception details and either terminate or present users with a generic "Whoops" and let them retry the operation.
   {
       Console.WriteLine("An unexpected error occurred.")
   }
}

This means that your business logic (validating) can be unit tested without a hard dependency on the data source or outputs. (Console) The code should be succinct and easy to understand what is being done. A Try/Catch can be added to GetInput to handle the exceptional case. Generally let exceptions bubble to a high-enough level to handle them.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM