9
\$\begingroup\$

I've just written some code in an online compiler, and since I'm very new to C#, I wanted to ask what could be improved about the code.

The task of the program is simply to add up the numbers the user entered before and then calculate the average.

public static void Main()

{
    int input;
    int sum = 0;
    bool error = false;
    int[] numbers = new int[5];

    for(int i = 0; i < numbers.Length; i++)
    {

        if (!error)
            Console.WriteLine("Please write a number:");

        if(Int32.TryParse(Console.ReadLine(), out input))
        {
            numbers[i] = input;
            sum += numbers[i];
            error = false;
        }
        else
        {
            Console.WriteLine("An error occured, please try again:");
            error = true;
            i--;
            continue;
        }
    }

    Console.WriteLine("");
    Console.WriteLine("Your typed in values:");
    for(int i = 0; i < numbers.Length; i++)
    {
        Console.WriteLine(numbers[i]);
    }

    Console.WriteLine("");

    Console.WriteLine("The result of your numbers:");
    Console.WriteLine(sum);

    Console.WriteLine("The average of your numbers:");
    Console.WriteLine((double)sum / (double)numbers.Length);
}

The code works fine, but maybe I could do some things more effectively.
Would be glad to get some help :)

Just to sum things up here is the console-output:

Please write a number:
> 4
Please write a number:
> 2
Please write a number:
> a
An error occured, please try again:
> 6
Please write a number:
> 4
Please write a number:
> 8

Your typed in values:
4
2
6
4
8

The sum of your numbers:
24
The average of your numbers:
4.8

('>' = User input)

\$\endgroup\$
11
\$\begingroup\$

The code works, and it seems that you're take care of invalid input. But the logic of the workflow is not - well - that logic.

int[] numbers = new int[5];

Here 5 is a magic number. Why five? Why not four or six or 20?

You should announce the number of input before asking for it (and maybe why).

Alternatively you could let the user enter a number of numbers to be calculated.


int sum = 0;

You probably have the sum variable for optimization reasons. Alternatively you could let the built in extension Sum() handle that:

numbers.Sum();

if (!error)

You have this negation of a "negative" variable. Why not be positive and call it success:

if (success)
  Console.Write("Enter Number: ");

Instead of:

    if (Int32.TryParse(Console.ReadLine(), out input))
    {
      numbers[i] = input;
      sum += numbers[i];

you could write:

    success = Int32.TryParse(Console.ReadLine(), out input);

    if (success)
    {
      ...
    }
    else
    {
      ...
    }

      continue;

I don't see any reason for calling continue in your loop? You use continue to step over the rest of the loop and reenter it - if the condition is still true, but here it is the last expression in the loop, so there is nothing to skip.


According to the overall design you should always split your code into meaningful methods in order to accommodate to principles like DRY and Single Responsibility:

For instance:

void Main()
{
  if (!GetCountOfNumbers(out int numCount))
    return;

  if (!GetUserInput(numCount, out int[] numbers, out int sum))
    return;

  PrintValues(numbers);
  Console.WriteLine("");
  PrintResult(sum, numbers.Length);
  Console.WriteLine("");
}

Where:

private bool GetCountOfNumbers(out int numCount)
{
  if (!GetIntInput("Enter count of Numbers", "Must be a positive number", x => x > 0, out numCount))
  {
    Console.WriteLine("User aborted the calculation");
    return false;
  }

  return true;
}

and

private bool GetUserInput(int numCount, out int[] numbers, out int sum)
{
  numbers = new int[numCount];
  sum = 0;
  int input;

  for (int i = 0; i < numCount; i++)
  {
    if (!GetIntInput($"Enter number {i + 1}", "Enter any valid integer (32 bit) value", x => true, out input))
    {
      Console.WriteLine("User aborted the calculation");
      return false;
    }

    numbers[i] = input;
    sum += input;
  }

  return true;
}

Both methods are calling GetIntInput() which could be defined as follows:

private bool GetIntInput(string message, string errorMessage, Predicate<int> predicate, out int result)
{
  result = default;

  while (true)
  {
    Console.Write($"{message} ['q' to Exit]: ");
    string input = Console.ReadLine();
    if (input == "q" || input == "Q")
      return false;

    if (int.TryParse(input, out int number) && predicate(number))
    {
      result = number;
      return true;
    }
    Console.WriteLine($"Invalid input: {errorMessage}");
  }
}

It takes a message to the user, a predicate to validate the input against and an error message if the predicate returns false on the input, and it returns the result in an out variable (result). Further it allows the user to exit the input loop and then returns true or false accordingly.

In this design everything is counted for through descriptive method names and the logic in the workflow is easily understood and separately maintainable.

It could surely be done in more sophisticated ways, but as a simple user input driven console application like this, the above is a simple way to structure the code properly.

\$\endgroup\$
7
\$\begingroup\$

Henkrik's answer is very good. However, there are some things I feel should be addressed as you are a beginner. Some of this answer will address things introduced in his answer.

First, out parameters should generally be avoided. They are used to force the method to initialize them before it returns; essentially, they are are saying "I have this value, but I need you to update it before you finish running so I can read the updated value." Normally you would use a return type for that. In special cases (as in int.TryParse) you need two values, but those are rare, and are mainly for external-facing APIs when you don't want to introduce and maintain a full type for two values to be used in one place. For internal methods, you should consider a tuple type with named members (introduced in C# 7). Having more than 1 out parameter typically is a sign that a more structured response (i.e. a type) is needed.

Henrik is right that you should pull your logic into multiple methods. However, each method should only perform one piece of work. One method, for example, could take input and return an int[]. Then you should have another function for performing the Sum and another the Average of these numbers. Then you can reuse your work. Performance will be slightly less due to calculating Sum twice, but until you hit the scale of millions of numbers, it won't be a serious problem. As an example implementation (using Linqs Sum method):

private int Sum(int[] args)
{
    return args.Sum();
}

private double Average(int[] args)
{
    return Sum(args) / args.Length;
}

You can define other methods, such as one to input a single number, one that calls that function several times to input a series of numbers, etc. Then we can call these methods all together and find the data we need.

As mentioned in a comment, I could have used Linq's Average feature: args.Average(). However, I left it with the call to Sum to show how it's good to make functions reusable.


Console.WriteLine(""); doesn't need the argument. If you want just a newline character fed into the output stream, you can do Console.WriteLine().

If you want output without a trailing newline, you can use Console.Write.


In a for loop, you almost never want to step back (i--). I would have used a for loop to get each number, and a while loop within it to get a valid number and keep retrying after invalid numbers were entered. Because you need to run the code at least once, a do/while loop would be a good choice, since it clearly expresses that the loop will run one time before the condition is checked. Something like:

for (var i = 0; i < 5; i++)
{
    var hasValidInput = false;
    do {
        // read input
        // set hasValidInput
    } while (!hasValidInput);
}

What you did right:

You had error handling if the user didn't input a number. That's a common issue for new programmers.

\$\endgroup\$
  • 1
    \$\begingroup\$ just one remark: csharp-examples.net/linq-average \$\endgroup\$ – dfhwze Aug 30 at 18:08
  • 1
    \$\begingroup\$ Isn't your "out-quote" what a ref-parameter would say? A ref-parameter is perfect for creating side effects. I would never use an out-parameter to update an existing value but only for providing a result from the method. As I understand it, I think your arguments are valid in a functional programming paradigme with pure functions, but not necessarily in a object oriented. \$\endgroup\$ – Henrik Hansen Aug 30 at 19:47
  • 1
    \$\begingroup\$ The thing is, out is for providing a value--but it's still a side-effect to an existing variable. It's basically a special case of ref, like a square is a special case of a rectangle. \$\endgroup\$ – Hosch250 Aug 30 at 19:49
  • 1
    \$\begingroup\$ Yes, but where a ref may or may not be updated, you know that an out will always be, so after the method returns you know the variable provided as an out parameter is changed (which - admitted - could be to it's existing value) - but you should handle it as changed, hence it acts just as a return value. \$\endgroup\$ – Henrik Hansen Aug 30 at 20:07
  • 2
    \$\begingroup\$ You beat me to the punch. My mind immediately jumped to the functions in the Linq namespace. \$\endgroup\$ – RubberDuck Aug 30 at 21:34
6
\$\begingroup\$

In the loop, you're trying to modify the iterator at 2 places. One here for(int i = 0; i < numbers.Length; i++) and the other inside the loop in the else block i--;. Reason I'm not inclined to do that is because not only for is maintaining the status of the iterator but the code as well. The question that needs to be asked is for as a looping construct suited for this problem. This is my take

while (i < 5) {
    Console.WriteLine("Please write a number:");
    if(Int32.TryParse(Console.ReadLine(), out input))
    {
        numbers[i++] = input;
        sum += input;
    }
    else
    {
        Console.WriteLine("An error occured, please try again");        
    }
}
\$\endgroup\$
3
\$\begingroup\$

I’m not a C# guy, but I can suggest that an error message should say something about the error. “Error” says as much as “an error occurred.” Since you tried to read an integer, perhaps “Integer only, please.”

Speaking of which, since you are only accepting integers, perhaps the prompt should say “integer” instead of “number.”

\$\endgroup\$
3
\$\begingroup\$

Consistency

You are evaluating the sum in a single pass by simply adding each input to the current sum. But you don't do the same for the average.

if(Int32.TryParse(Console.ReadLine(), out input))
{
    numbers[i] = input;
    sum += numbers[i];
}

// ..

Console.WriteLine("The result of your numbers:");
Console.WriteLine(sum);
Console.WriteLine("The average of your numbers:");
Console.WriteLine((double)sum / (double)numbers.Length);

As others have pointed out, you should just store the numbers in a single pass numbers[i] = input; and then evaluate the sum and average each in their own pass as:

var sum = numbers.Sum();
var average = numbers.Average();

Cumulative Moving Average

There is another way to be consistent, while keeping everything in a single pass. You'd have the build a cumulative moving average together with the sum:

if(Int32.TryParse(Console.ReadLine(), out input))
{
    numbers[i] = input;
    sum += numbers[i];
    average += ((decimal)input - average) / (i + 1);
}

I would use a decimal to mitigate rounding and truncation errors.

\$\endgroup\$
  • 1
    \$\begingroup\$ Just a thought, instead of computing average on each pass can't we just do sum/numbers.Length. Reason I'm mentioning that is though the above idea you mentioned above about cumulative moving average is very good, but my questions are as you mentioned we need to mitigate rounding errors, and the other will be by just doing sum/numbers.Length will be doing only one computation as compared to calculating average on every iteration. \$\endgroup\$ – Chaitanya Sep 1 at 5:14
  • \$\begingroup\$ Sure that could also work. My point was only that used a one-pass sum, and a two pass average. The way you implement this is up to you of course :) \$\endgroup\$ – dfhwze Sep 1 at 7:06
1
\$\begingroup\$

I'm wondering why are you using an array to store your input in, this a bit of overkill if you only need to have the sum and the average, you can store this in 3 variables. Have a look at this.

It shows how to handle the user canceling the console as well as eliminating the need for an array or list. You can still break when it has reached 5 entries by limiting the tries or limiting the successful conversions.

If the user makes a mistake it is normally a good idea to show him what he typed else he is confused and was sure what he typed was "correct ish"

public static void Main()

{

    int sum = 0;
    bool error = false;
    int count = 0;
    bool canceled = false;
    var track = new StringBuilder();
    track.Append("Your typed in values: ");
    Console.CancelKeyPress += (s, e) => canceled = true;

    (int left, int top) = (Console.CursorLeft, Console.CursorTop);
    Console.WriteLine("Please write a number, press CTL + C to cancel:");

    do
    {
        var userData = Console.ReadLine();
        Console.SetCursorPosition(left, top);
        if (!canceled && int.TryParse(userData, out int input))
        {
            count++;
            sum += input;   
            if(count>1)
                Console.WriteLine(track.Append(","));

            //clear as the user can endless type values
            Console.Clear();
            Console.WriteLine("Please write a number, press CTL + C to cancel:");
            Console.WriteLine(track.Append(userData));
        }
        else
        {

            Console.WriteLine($"Sorry could not convert {userData} to an integer press CTL + C to cancel to cancel or try again".PadRight(Console.BufferWidth,' '));

        }

    } while (!canceled && !error);



                Console.WriteLine("");

    Console.WriteLine("The result of your numbers:");
    Console.WriteLine(sum);

    Console.WriteLine("The average of your numbers:");
    Console.WriteLine((double)sum / (double)count);
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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