Содержание
Задача: пользователь вводит в консоль целые числа. После ввода очередного числа и нажатия кнопки «Enter» программа должна рассчитать среднее значение всех введенных чисел и вывести это значение в консоль. Признаком прекращения работы программы является нажатие на клавиатуре кнопки «q».
Анализ задачи
По условиям задачи пользователь должен задавать только целочисленных значения. К целочисленным типам в C# относятся такие типы данных как int
, long
, byte
и так далее. Для решения задачи будем использовать наиболее часто используемый тип данных — int
.
Сразу стоит отметить практическую ценность этой лабораторной работы. Замените условие ввода чисел пользователем в консоль на любой другой источник данных, например, на показания электронного термометра, которые поступают в приложение с заданным интервалом времени или непрерывно и нам необходимо выводить пользователю среднее значение температуры. Или на показания GPS-навигатора по которым мы должны вывести среднюю скорость движения и так далее. А условием прекращения расчёта среднего значение будет, например, отсутствие новых показаний в течение 10 секунд. По сути, в этой лабораторной работе мы и будем учиться рассчитывать среднее значение какой-то величины.
Что касается расчёта среднего значения, то, в данном случае, будем считать, что от нас требуется рассчитать среднее арифметическое значение, которое вычисляется путем сложения группы чисел, а затем деления на количество этих чисел. Например, средним значением для чисел 2, 3, 3, 5, 7 и 10 будет 5, которое является результатом деления их суммы, равной 30, на их количество, равное 6.
Но, проблема заключается в том, что мы не знаем заранее сколько чисел задаст пользователь — может задать одно значение, миллион или вообще ни одного, сразу нажав клавишу «q». Рассмотрим несколько вариантов работы нашего приложения.
Вариант 1. Пользователь не вводит ни одного числа и сразу нажимает клавишу «q». Какой результат должна выдать в этом случае программа? Договоримся, что это будет значение 0
Вариант 2. Пользователь вводит одно число. Например, пользователь вводит значение 5. Очевидно, что приложение должно вернуть это же число — 5
Вариант 3. Пользователь вводит последовательно несколько чисел. Например, пользователь вводит значения 1
, 2
и 3
. Программа должна сработать следующим образом:
- Шаг №1. Пользователь вводит значение
1
и нажимает Enter. Программа должна вернуть значение1
(см. вариант №2 работы программы) - Шаг №2. Пользователь вводит значение
2
и нажимает Enter. Программа должна уже рассчитать среднее значение:(1+2)/2 = 1,5
- Шаг №3. Пользователь вводит значение
3
и нажимает Enter. Программа должна рассчитать новое среднее значение:(1+2+3)/3 = 2
Соответственно, если пользователь вводит последовательно 20 значений, то при каждом очередном вводе числа программа должна рассчитать новое среднее значение.
Второй момент на который стоит обратить внимание — это условие выхода из программы. Пользователь должен нажать клавишу «q». Следовательно до того, как пользователь нажмет эту клавишу программа должна работать и ожидать от пользователя ввода очередного целого числа.
Исходя из вышесказанного, нам необходимо научиться сразу двум моментам работы с консольными приложениями:
- научиться считывать и анализировать нажатые пользователем клавиши на клавиатуре
- непосредственно научиться считать среднее арифметическое значение заранее неизвестного ряда чисел
Исходя из этих двух моментов и будем разрабатывать наше приложение.
Анализ нажатых пользователем клавиш в консоли
Когда нам необходимо получить и проанализировать нажатие пользователем отдельной клавиши на клавиатуре, то мы можем воспользоваться таким методом класса Console
как ReadKey()
Метод ReadKey()
возвращает специальную структуру ConsoleKeyInfo
, которая содержит как информацию о нажатой клавише, так и об используемых модификаторах — Ctrl, Alt или Shift. При этом, символ нажатой клавиши отображается в консоли. Получив в свое распоряжение структуру ConsoleKeyInfo
, мы можем узнать был ли нажат какой-либо символ на клавиатуре или же была нажата какая-то другая клавиша, например, Backspace или Escape и так далее. Чтобы продемонстрировать как работает метод ReadKey()
и, заодно, начать работу над нашей программой, создадим новое консольное приложение и напишем следующий код метода Main():
namespace ConsoleApp2 { internal class Program { static void Main(string[] args) { bool readNext = true; while (readNext) { ConsoleKeyInfo info = Console.ReadKey(); switch (info.Key) { case ConsoleKey.Q: { readNext = false; break; } case ConsoleKey.Enter: { Console.WriteLine("Нажата клавиша Enter"); break; } default: { Console.WriteLine("Нажата какая-то клавиша на клавиатуре, но не Enter или Q"); break; } } } } } }
Здесь мы запускаем цикл while
, который будет работать до тех пор, пока переменная readNext
не станет равной false
. Внутри цикла мы считываем следующую нажатую клавишу:
ConsoleKeyInfo info = Console.ReadKey();
и анализируем её, используя в условном операторе switch
значение поля Key
структуры ConsoleKeyInfo
:
switch (info.Key) { ... }
Так, мы проверяем три условия — пользователь нажал на клавиатуре клавишу Q, клавишу Enter или любую другую клавишу. При этом, если пользователь нажимает клавишу Q, то переменной readNext
присваивается значение false
и цикл завершается, тем самым завершая и работу приложения. Теперь запустите приложение и попробуйте нажать какие-нибудь клавиши на клавиатуре. В итоге, вы должны увидеть в консоли, примерно такой вывод:
Здесь на рисунке красным выделены нажатые на клавиатуре символы. Даже, если вы нажмете на клавиатуре, например, Backspace, то приложение не удалит последний введенный символ, а выведет сообщение как показано на рисунке выше. Следовательно, раз мы будем использовать ReadKey()
в своей работе, то нам необходимо дополнительно позаботиться о том, каким образом мы будем обрабатывать нажатые на клавиатуре клавиши, например, позволим ли мы пользователю удалять символы из введенной строки или будем ли мы позволять пользователю выводить в консоль что-то кроме чисел?
Давайте доработаем немного наше приложение и сделаем так, чтобы в консоли отображались только действия от нажатия следующих клавиш — Q, Enter, Backspace и цифровые клавиши от 0 до 9. Вот как может выглядеть наше приложение:
namespace ConsoleApp2 { internal class Program { static void Main(string[] args) { bool readNext = true; string numbers = "1234567890"; while (readNext) { ConsoleKeyInfo info = Console.ReadKey(); switch (info.Key) { case ConsoleKey.Q: { readNext = false; break; } case ConsoleKey.Enter: { Console.WriteLine(); Console.WriteLine("Нажата клавиша Enter"); break; } case ConsoleKey.Backspace: { Console.Write(" \b"); break; } default: { if (numbers.Contains(info.KeyChar)==false) Console.WriteLine("\rВы пытаетесь задать не число"); break; } } } } } }
Посмотрим на изменения. Во-первых, мы добавили в switch
новое условие — нажатие клавиши Backspace:
case ConsoleKey.Backspace: { Console.Write(" \b"); break; }
Обратите внимание, что мы здесь самостоятельно удаляем последний символ из строки в консоли, используя одну из escape-последовательностей (\b
). Попробуйте закомментировать вызов Console.Write(" \b");
и вы увидите, что при нажатии Backspace курсор в консоли будет перемещаться на один символ влево, но ничего удаляться не будет.
Во-вторых, у нас появилась новая переменная — строка:
string numbers = "1234567890";
которую мы используем для того, чтобы проверить нажатую клавишу в ветке default
оператора switch
.
default: { if (numbers.Contains(info.KeyChar)==false) Console.WriteLine("\rВы пытаетесь задать не число"); break; }
здесь мы используем ещё одно поле структуры ConsoleKeyInfo
— KeyChar
, то есть символьное представление нажатой на клавиатуре клавиши. Мы проверяем содержит ли строка numbers
нажатый символ и, если такого символа нет в строке, то выводим сообщение о том, что пользователь нажимает недопустимую клавишу.
Теперь у нас есть готовый «скелет» приложения — мы можем различать какие клавиши нажимает пользователь и, при необходимости, выводить пользователю сообщения об ошибках. Остается открытым вопрос — как при такой работе приложения правильно прочитать введенное пользователем число? Число должно передаваться в приложение только после того, как пользователь нажмет клавишу Enter. До этого момента мы должны запоминать какие цифры нажимались. Например, пользователь может выполнить следующий набор действий:
- нажать клавиши 1 2 3 5,
- нажать один раз Backspace
- нажать клавиши 4 и 1
- нажать Enter
В этом случае мы должны передать в приложение именно число 12341, а не 123541 и не в коем случае не набор цифр — 1, 2, 3, 5, 4, 1. Допишем наше приложение следующим образом:
namespace ConsoleApp2 { internal class Program { static void Main(string[] args) { bool readNext = true; string numbers = "1234567890"; string str = ""; while (readNext) { ConsoleKeyInfo info = Console.ReadKey(); switch (info.Key) { case ConsoleKey.Q: { readNext = false; break; } case ConsoleKey.Enter: { Console.WriteLine(); if (int.TryParse(str, out int aNext)) { Console.WriteLine($"Вы ввели число {aNext}"); str = ""; } break; } case ConsoleKey.Backspace: { Console.Write(" \b"); if (str.Length > 0) str = str.Remove(str.Length - 1, 1); break; } default: { if (numbers.Contains(info.KeyChar) == false) Console.WriteLine("\rВы пытаетесь задать не число"); else str += info.KeyChar; break; } } } } } }
Снова посмотрим на изменения в приложении. Итак, в приложении появилась новая переменная:
string str = "";
в которой мы храним нажатые и допустимые символы, то есть, в нашем случае, это будут только цифры. Но храним мы их именно как строку. Очередной символ сохранятся в этой строке при выполнении ветки default:
default: { if (numbers.Contains(info.KeyChar) == false) Console.WriteLine("\rВы пытаетесь задать не число"); else str += info.KeyChar; //сохраняем нажатую цифру в переменной break; }
Второе изменение касается случая, когда пользователь нажимает Backspace. Здесь мы помимо того, что должны удалить символ из консоли, мы также должны удалить этот же символ из переменной, что мы и делаем:
case ConsoleKey.Backspace: { Console.Write(" \b"); //удаляем символ из консоли if (str.Length > 0) str = str.Remove(str.Length - 1, 1); //удаляем последний символ из переменной break; }
При этом, мы также обрабатываем ситуацию, когда строка полностью стерта из консоли и, следовательно, в переменной str
хранится ноль символов (пустая строка), но пользователь всё равно жмет Backspace. Для того, чтобы приложение не выдало нам ошибку о том, что мы пытаемся использовать отрицательное значение индекса символа в строке, мы обязательно проверяем условие, что в строке есть хотя бы один символ для удаления:
if (str.Length > 0) //есть хотя бы один символ - значит можем попытаться удалить его из строки
и, наконец, если пользователь жмет Enter, то мы пытаемся преобразовать строку str
в число и показать это число пользователю.
case ConsoleKey.Enter: { Console.WriteLine(); if (int.TryParse(str, out int aNext)) { Console.WriteLine($"Вы ввели число {aNext}"); str = ""; } break; }
Здесь стоит обратить внимание на то, что, во-первых, так как обрабатывается Backspace, то str
может быть пустой строкой и, чтобы, опять же, не получить ошибку преобразования строки в число, мы используем метод int.TryParse()
, который просто вернет нам false, если строку не удастся преобразовать в целое число. А, во-вторых, мы обязательно после преобразования строки в число стираем всё, что было записано в переменной str
.
str = "";
иначе, если не «забыть» значение строки, то приложение будет каждый раз выдавать очередное число на порядки больше предыдущего. Например, пользователь нажмет 1 и Enter — приложение выдаст число 1. Затем пользователь нажмет 2 и Enter, двойка добавиться к уже имеющейся единице и str
будет содержать «12» — приложение выдаст число 12 и так далее.
Теперь можно снова запустить приложение и убедиться, что приложение корректно обрабатывает нажатие клавиш и показывает введенные пользователем целые числа:
Обратите внимание, что, даже, если установлена раскладка отличная от английской, то код клавиши Q всё равно обрабатывается корректно и приложение завершает свою работу.
Вот теперь у нас всё готово для того, чтобы рассчитать среднее арифметическое значение всех введенных пользователем чисел.
Расчёт среднего арифметического значения неизвестного заранее ряда
Стоит отметить, что можно предложить несколько различных вариантов расчёта среднего арифметического значения. Например, мы можем где-ибо хранить полученные значения, например, в списке или даже записывать полученные числа в отдельный файл и каждый раз читать этот файл. Мы же рассмотрим вариант при котором сам по себе ряд значений нигде не храниться вообще. Такой подход удобен тем, что мы не храним в приложении ничего лишнего — только среднее значение и одно дополнительное число. Суть заключается в том, что для расчёта среднего арифметического значения нам не требуется знать ничего, кроме трех значений:
- Предыдущее среднее арифметическое значение
- Количество элементов ряда (читай — сколько раз пользователь нажал Enter)
- Очередное число ряда
В этом случае, новое среднее арифметическое значение будет рассчитываться по следующей формуле:
здесь X — новое среднее арифметическое значение, Xi-1 — среднее арифметическое значение, полученное на предыдущем шаге расчёта, n — количество элементов ряда, a
— очередное значение ряда. Всё, что нам остается — это применить представленную формулу в нашем приложении:
namespace ConsoleApp2 { internal class Program { static void Main(string[] args) { double avg = 0; double n = 0; bool readNext = true; string numbers = "1234567890"; string str = ""; while (readNext) { ConsoleKeyInfo info = Console.ReadKey(); switch (info.Key) { case ConsoleKey.Q: { readNext = false; break; } case ConsoleKey.Enter: { Console.WriteLine(); if (int.TryParse(str, out int aNext)) { avg = avg * (n / (n + 1)) + aNext / (n + 1); n++; Console.WriteLine($"Среднее значение {avg}"); str = ""; } break; } case ConsoleKey.Backspace: { Console.Write(" \b"); //удаляем символ из консоли if (str.Length > 0) str = str.Remove(str.Length - 1, 1); //удаляем последний символ из переменной break; } default: { if (numbers.Contains(info.KeyChar) == false) Console.WriteLine("\rВы пытаетесь задать не число"); else str += info.KeyChar; //сохраняем нажатую цифру в переменной break; } } } } } }
Здесь мы добавили две новые переменные, которым сразу присвоили начальные значения:
double avg = 0; //среднее арифметическое double n = 0; //количество элементов ряда
и применили формулу при нажатии Enter:
case ConsoleKey.Enter: { Console.WriteLine(); if (int.TryParse(str, out int aNext)) { avg = avg * (n / (n + 1)) + aNext / (n + 1); n++; Console.WriteLine($"Среднее значение {avg}"); str = ""; } break; }
количество элементов ряда увеличивается каждый раз на 1 после того, как рассчитано очередное среднее арифметическое значение
avg = avg * (n / (n + 1)) + aNext / (n + 1); n++;
Теперь запустим приложение и проверим его работу на примере представленного выше ряда чисел: 2, 3, 3, 5, 7 и 10
Как видите, приложение прекрасно справляется с расчётом. Нетрудно догадаться, что, если вам необходимо будет сохранить весь введенный ряд чисел, то делать это необходимо будет в той же ветке switch, обрабатывающей нажатие Enter.
Итого
В этой лабораторной работе мы научились работать с методом ReadKey()
класса Console
и рассчитывать среднее арифметическое значение заранее неизвестного ряда чисел. Для достижения поставленной цели нам потребовалось умение пользоваться циклами, использовать оператор switch
, escape-последовательности в строках, а также, непосредственно, использовать методы для работы со строками.