Лабораторная работа №1: Работа с CSV файлами на C#

Лабораторная №1 «Работа с файлами»
Задание на лабораторную работу
Разработать консольное приложение для работы с данными из текстового файла в
формате CSV. Используемая среда разработки MS Visual Studio 2013, язык – C#.
Требования к выполнению лабораторной работы
Данные в файле должны соответствовать следующим требованиям:
 несколько текстовых полей;
 одно (несколько) числовое поле;
 одно (несколько) логическое поле.
Примеры определяются согласно варианту, указанному преподавателем.
Разработанное приложение должно обеспечивать следующие возможности:
 Вывод всех записей на экран
 Вывод записи по номеру
 Запись данных в файл
 Удаление записи (записей) из файла
 Добавление записи в файл
Интерфейс приложения должен быть выполнен в текстовом виде.
Выбор возможных операций с файлом организуется при помощи меню. Выход из
приложения осуществляется пользователем по клавише Esc.
Сущность, выбранная согласно варианту задания, должна быть реализована в виде
класса. Класс должен быть определен в отдельном файле.
При реализации программы следует использовать стандарт кодирования:
https://msdn.microsoft.com/en-us/library/ff926074.aspx
Предусмотреть все возможные проверки на введение данных, несовпадающих по
формату.
Данные, хранимые в файле должны быть приближены к реальным (не допускается
если во время защиты лабораторной работы приложение содержит поля: фыва, asdzxcv,
11111!!! И т.п). Для защиты лабораторной работы требуется наполнить файл не менее чем
10-ю записями.
Методические указания к выполнению лабораторной работы
CSV
CSV (от англ. Comma-Separated Values — значения, разделённые запятыми) —
текстовый формат, предназначенный для представления табличных данных. Каждая строка
файла — это одна строка таблицы. Значения отдельных колонок разделяются
разделительным символом (delimiter) — запятой (,). Однако, большинство программ вольно
трактует стандарт CSV и допускают использование иных символов в качестве разделителя.
Значения, содержащие зарезервированные символы (двойная кавычка, запятая, точка с
запятой, новая строка) обрамляются двойными кавычками ("); если в значении встречаются
кавычки — они представляются в файле в виде двух кавычек подряд. Строки разделяются
парой символов CR LF (0x0D 0x0A) (в DOS и Windows эта пара генерируется нажатием
клавиши Enter). Однако конкретные реализации могут использовать другие общепринятые
разделители строк, например, LF (0x0A) в UNIX.
Чтение и запись из файла
.NET фреймворк позволяет работать с файлами различными способами. Самым
удобным способом в данном случае будет использование классов StreamReader и
StreamWriter из пространства имён System.IO. Пространство имён подключается с
помощью директивы using.
using System.IO;
…
using (var sw = new StreamWriter(path))
{
// Запись в файл
sw.WriteLine("This");
}
using (var sr = new StreamReader(path))
{
string str;
while ((str = sr.ReadLine()) != null)
{
// Необходимые действия со строкой
}
}
Для неявной типизации вместо названия типа данных используется ключевое слово
var. Затем уже при компиляции компилятор сам выводит тип данных исходя из
присвоенного значения.
var stroka = "Hellо to World";
var c = 20;
И при чтении, и при записи используется оператор using. Не надо путать данный
оператор с директивой using, которая подключает пространства имен в начале файла кода.
Оператор using позволяет создавать объект в блоке кода, по завершению которого
вызывается метод Dispose у этого объекта, и, таким образом, объект уничтожается.
Чтение из файла и StreamReader
Класс StreamReader позволяет нам легко считывать весь текст или отдельные
строки из текстового файла. Среди его методов можно выделить следующие:
Close: закрывает считываемый файл и освобождает все ресурсы
Peek: возвращает следующий доступный символ, если символов больше нет, то
возвращает -1
Read: считывает и возвращает следующий символ в численном представлении. Имеет
перегруженную версию: Read(char[] array, int index, int count), где array - массив,
куда считываются символы, index - индекс в массиве array, начиная с которого
записываются считываемые символы, и count - максимальное количество считываемых
символов
ReadLine: считывает одну строку в файле
ReadToEnd: считывает весь текст из файла
Считаем текст из файла различными способами:
string path = @"D:\text.txt";
try
{
Console.WriteLine("******считываем весь файл********");
using (StreamReader sr = new StreamReader(path))
{
Console.WriteLine(sr.ReadToEnd());
}
Console.WriteLine();
Console.WriteLine("******считываем построчно********");
using (StreamReader sr = new StreamReader(path, System.Text.Encoding.Default))
{
string line;
while ((line = sr.ReadLine()) != null)
{
Console.WriteLine(line);
}
}
Console.WriteLine();
Console.WriteLine("******считываем блоками********");
using (StreamReader sr = new StreamReader(path, System.Text.Encoding.Default))
{
char[] array = new char[4];
// считываем 4 символа
sr.Read(array, 0, 4);
Console.WriteLine(array);
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
В первом случае мы разом считываем весь текст с помощью метода ReadToEnd().
Во втором случае считываем построчно через цикл while:
while ((line = sr.ReadLine()) != null) - сначала присваиваем переменной line
результат функции sr.ReadLine(), а затем проверяем, не равна ли она null. Когда объект
sr дойдет до конца файла и больше строк не останется, то метод sr.ReadLine() будет
возвращать null.
В третьем случае считываем в массив четыре символа.
Обратите внимание, что в последних двух случаях в конструкторе StreamReader
указывалась кодировка System.Text.Encoding.Default. Свойство Default класса
Encoding получает кодировку для текущей кодовой страницы ANSI. Также через другие
свойства мы можем указать другие кодировки. Если кодировка не указана, то при чтении
используется UTF8. Иногда важно указывать кодировку, так как она может отличаться от
UTF8, и тогда мы получим некорректный вывод.
Запись в файл и StreamWriter
Для записи в текстовый файл используется класс StreamWriter. Свою
функциональность он реализует через следующие методы:
Close: закрывает записываемый файл и освобождает все ресурсы
Flush: записывает в файл оставшиеся в буфере данные и очищает буфер.
Write: записывает в файл данные простейших типов, как int, double, char,
string и т.д.
WriteLine: также записывает данные, только после записи добавляет в файл символ
окончания строки
Рассмотрим запись в файл на примере:
string readPath = @"D:\text.txt";
string writePath = @"D:\textw.txt";
string text = "";
try
{
using (StreamReader sr = new StreamReader(readPath,
System.Text.Encoding.Default))
{
text = sr.ReadToEnd();
}
using (StreamWriter sw = new StreamWriter(writePath, false,
System.Text.Encoding.Default))
{
sw.WriteLine(text);
}
using (StreamWriter sw = new StreamWriter(writePath, true,
System.Text.Encoding.Default))
{
sw.WriteLine("Дозапись");
sw.Write(4.5);
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Здесь сначала мы считываем файл в переменную text, а затем записываем эту
переменную в файл, а затем через объект StreamWriter записываем в новый файл.
Класс StreamWriter имеет несколько конструкторов. Здесь мы использовали один
из них:
new StreamWriter(writePath, false, System.Text.Encoding.Default)
В качестве первого параметра передается путь к записываемому файлу. Второй
параметр представляет булевую переменную, которая определяет, будет файл
дозаписываться или перезаписываться. Если этот параметр равен true, то новые данные
добавляются в конце к уже имеющимся данным. Если false, то файл перезаписывается. И
если в первом случае файл перезаписывается, то во втором делается дозапись в конец
файла.
Третий параметр указывает кодировку, в которой записывается файл.
Свойства класса
Свойство — это член, предоставляющий гибкий механизм для чтения, записи или
вычисления значения частного (private) поля. Свойства можно использовать, как если
бы они являлись открытыми членами данных, хотя в действительности они являются
специальными методами, называемыми методами доступа (accessor). Это обеспечивает
простой доступ к данным и позволяет повысить уровень безопасности и гибкости методов.
Существует два таких метода: get (для получения данных) и set (для записи).
Объявление простого свойства имеет следующую структуру:
[модификатор доступа] [тип] [имя_свойства]
{
get
{
// тело аксессора для чтения из поля
}
set
{
// тело аксессора для записи в поле
}
}
Пример – класс Test с различными свойствами:
public class Test
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
public int Age { get; set; }
public string ToPrint
{
get { return Name + " " + Age.ToString(); }
}
public int Cnt { get; private set; }
}
Свойства класса удобно использовать в случае если нужна проверка присваиваемого
значения.
Рассмотрим пример использования свойств. Имеется класс Студент, и в нем есть
закрытое поле курс, которое не может быть ниже единицы и больше пяти. Для управления
доступом к этому полю будет использовано свойство Year:
class Student
{
private int year; //объявление закрытого поля
public int Year //объявление свойства
{
get // аксессор чтения поля
{
return year;
}
set // аксессор записи в поле
{
if (value < 1)
year = 1;
else if (value > 5)
year = 5;
else year = value;
}
}
}
class Program
{
static void Main(string[] args)
{
Student st1 = new Student();
st1.Year = 0; // записываем в поле, используя аксессор set
Console.WriteLine(st1.Year); // читаем поле, используя аксессор get, выведет 1
Console.ReadKey();
}
}
Проще говоря, в свойстве реализуются два метода. В теле аксессора get может быть
более сложная логика доступа, но в итоге должно возвращаться значение поля, либо другое
значение с помощью оператора return. В аксессоре set же присутствует неявный
параметр value, который содержит значение, присваиваемое свойству (в примере выше,
при записи, значение value равно «0»).
Свойство также может предоставлять доступ только на чтение поля или только на
запись. Если, например, нам необходимо закрыть доступ на запись, мы просто не указываем
аксессор set. Пример:
class Student
{
private int year;
public Student(int y) // конструктор
{
year = y;
}
public int Year
{
get
{
return year;
}
}
}
class Program
{
static void Main(string[] args)
{
Student st1 = new Student(2);
Console.WriteLine(st1.Year); // чтение
st1.Year = 5; // ошибка, свойство только на чтение
Console.ReadKey();
}
}
Стоит помнить, что само свойство не определяет место в памяти для хранения поля,
и, соответственно, необходимо отдельно объявить поле, доступом к которому будет
управлять свойство.
Автоматическое свойство – это очень простое свойство, которое, в отличии от
обычного свойства, уже определяет место в памяти (создает неявное поле), но при этом не
позволяет создавать логику доступа. Структура объявления Автоматического свойства:
[модификатор доступа] [тип] [имя_свойства] { get; set; }
У таких свойств, у их аксессоров отсутствует тело. Пример использования:
class Student
{
public int Year { get; set; }
}
class Program
{
static void Main(string[] args)
{
Student st1 = new Student();
st1.Year = 0;
Console.WriteLine(st1.Year);
Console.ReadKey();
}
}
Автоматически реализуемые свойства есть смысл использовать тогда, когда нет
необходимости накладывать какие-либо ограничения на возможные значения неявного
поля свойства.
И тут может возникнуть вопрос, а в чем тогда разница между простыми открытыми
полями и автоматическими свойствами. У таких свойств остается возможность делать их
только на чтение или только на запись. Для этого уже используется модификатор доступа
private перед именем аксессора:
public int Year { private get; set; } // свойство только на запись
public int Year { get; private set; } // свойство только на чтение
Классы и методы, необходимые для выполнения работы
Класс Console.
Класс Console и его статические методы предназначены для работы с консолью.
Полезными при выполнении работы могут быть следующие методы:
Clear: очистка консоли
WriteLine: вывод строки текста, включая символ возврата каретки (то есть с
переводом на новую строку)
Write: вывод строки текста, но без символа возврата каретки
ReadLine: считывание строки текста со входного потока
Read: считывание введенного символа в виде числового кода данного символа. С
помощью преобразования к типу char мы можем получить введенный символ
ReadKey: считывание нажатой клавиши клавиатуры (ConsoleKeyInfo key=
Console.ReadKey();)
Пример: Приложение принимает два числа, введенные пользователем и отображает
их сумму.
try
{
do
{
Console.WriteLine("Введите первое число");
int num1 = Int32.Parse(Console.ReadLine());
Console.WriteLine("Введите второе число");
int num2 = Int32.Parse(Console.ReadLine());
Console.WriteLine("Сумма чисел {0} и {1} равна {2}", num1, num2, num1 + num2);
Console.WriteLine("Для выхода нажмите Escape; для продолжения - любую другую
клавишу");
}
while (Console.ReadKey().Key != ConsoleKey.Escape);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.ReadLine();
}
Помещаем весь используемый код в блок try, поскольку у нас может возникнуть
исключение в ходе преобразования строки в число (если мы введем нечисловые символы).
Непосредственно код программы представляет цикл do...while, из которого
можно выйти, только нажав на клавишу Escape.
С помощью метода Int32.Parse преобразуем введенную сроку в число: int num1 =
Int32.Parse(Console.ReadLine());
Описание класса Console –
http://msdn.microsoft.com/ru-ru/library/system.console(v=vs.110).aspx
Обработка исключительных ситуаций.
Функции обработки исключений на языке C# помогают обрабатывать любые
непредвиденные или исключительные ситуации, происходящие при выполнении
программы. При обработке исключений используются ключевые слова try, catch и
finally для попыток применения действий, которые могут не достичь успеха, для
обработки ошибок, если предполагается, что это может быть разумным, и для
последующего освобождения ресурсов. Исключения могут генерироваться средой CLR,
платформой .NET Framework или внешними библиотеками, либо кодом приложения.
Исключения создаются при помощи ключевого слова throw.
Во многих случаях исключение может инициироваться не методом, вызванным
непосредственно кодом, а другим методом, расположенным ниже в стеке вызовов. Когда
это происходит, среда CLR выполняет откат стека в поисках метода с блоком catch для
определенного типа исключения. При обнаружении первого такого блока catch этот блок
выполняется. Если среда CLR не находит соответствующего блока catch где-либо в стеке
вызовов, она завершает процесс и отображает пользователю сообщение.
Порядок использования конструкции try-catch-else:
try
{
// Действия при выполнении которых может возникнуть исключительная ситуация
}
catch (Exception e)
{
// Обработка исключения
}
finally
{
// Действия, которые будут выполнены в любом случае
}
Например:
try
{
int i = Convert.ToInt32(Console.ReadLine());
}
catch (Exception ex)
{
Console.WriteLine("Ошибка: " + ex.Message);
}
finally
{
Console.WriteLine("Блок finally");
}
При использовании блока try...catch..finally вначале выполняются все
инструкции между операторами try и catch. Если между этими операторами вдруг
возникает исключение, то обычный порядок выполнения останавливается и переходит к
инструкции сatch. В данном случае у нас может возникнуть исключение в блоке try, если
мы попытаемся ввести строку, содержащую нечисловые символы, выполнение программы
остановится и перейдет к блоку catch.
Инструкция catch имеет следующий синтаксис:
catch (тип_исключения имя_переменной)
В нашем случае объявляется переменная ex, которая имеет тип Exception. Но если
возникшее исключение не является исключением типа, указанного в инструкции сatch, то
оно не обрабатывается, а программа просто зависает или выбрасывает сообщение об
ошибке.
Однако так как тип Exception является базовым классом для всех исключений, то
выражение catch (Exception ex) будет обрабатывать практически все исключения. Вся
обработка исключения в нашем случае сводится к выводу на консоль сообщении об
исключении, которое в свойстве message класса Exception.
Далее в любом случае выполняется блок finally. Однако этот блок необязательный,
и его можно при обработке исключений опускать. Если же в ходе программы исключений
не возникнет, то программа не будет выполнять блок catch, сразу перейдет к блоку
finally, если он имеется.
При необходимости мы можем разграничить обработку различных типов
исключений, включив дополнительные блоки catch:
static void Main(string[] args)
{
try
{
}
catch (FileNotFoundException e)
{
// Обработка исключения, возникшего при отсутствии файла
}
catch (IOException e)
{
// Обработка исключений ввода-вывода
}
Console.ReadLine();
}
Если у нас возникает исключение определенного типа, то оно переходит к
соответствующему блоку catch.
При этом более частные исключения следует помещать в начале, и только потом более
общие классы исключений. Например, сначала обрабатывается исключение
IOException, и только потом Exception (так как IOException наследуется от класса
Exception).
Подробно https://msdn.microsoft.com/ru-ru/library/s7fekhdy.aspx
Обработка исключений и условные конструкции
Ряд исключительных ситуаций может быть предвиден разработчиком. Например,
пусть программа предусматривает ввод числа и вывод его квадрата:
Console.WriteLine("Введите число");
int x = Int32.Parse(Console.ReadLine());
x *= x;
Console.WriteLine("Квадрат числа: " + x);
Console.Read();
Если пользователь введет не число, а строку, какие-то другие символы, то программа
выпадет в ошибку. С одной стороны, здесь как раз та ситуация, когда можно применить
блок try..catch, чтобы обработать возможную ошибку. Однако гораздо оптимальнее было
бы проверить допустимость преобразования:
Console.WriteLine("Введите число");
int x;
string input = Console.ReadLine();
if (Int32.TryParse(input, out x))
{
x *= x;
Console.WriteLine("Квадрат числа: " + x);
}
else
{
Console.WriteLine("Некорректный ввод");
}
Console.Read();
Метод Int32.TryParse() возвращает true, если преобразование можно осуществить,
и false - если нельзя. При допустимости преобразования переменная x будет содержать
введенное число. Так, не используя try...catch можно обработать возможную
исключительную ситуацию.
С точки зрения производительности использование блоков try..catch более
накладно, чем применение условных конструкций. Поэтому по возможности вместо
try..catch лучше использовать условные конструкции на проверку исключительных
ситуаций.
Работа со строками
Строки в Си-шарп - это объекты класса String, значением которых является текст.
Для работы со строками в этом классе определено множество методов (функций),
рассмотрим некоторые из них.
Чтобы использовать строку, ее нужно сначала создать – присвоить какое-либо
значение.
Для объединения (конкатенации) строк используется оператор "+".
string s = "Hello," + " World!";
Оператор "[]" используется для доступа (только чтение) к символу строки по индексу:
string s = "Hello, World!";
char c = s[1]; // 'e'
Свойство Length возвращает длину строки.
Спецсимволы:
Символ "\" является служебным, поэтому, чтобы использовать символ обратного
слэша необходимо указывать его дважды "\\".
Символ табуляции – "\t"
Символ перевода строки – "\r\n"
Двойные кавычки – "\""
Методы (функции) класса String для работы со строками в Си-шарп:
Метод IsNullOrEmpty() возвращает True, если значение строки равно null, либо
когда она пуста (значение равно "").
Метод IsNullOrWhiteSpace() работает как и метод IsNullOrEmpty(), только
возвращает True еще и тогда, когда строка представляет собой набор символов пробела
и/или табуляции ("\t").
Для сравнивания строк используется метод Compare(). Суть сравнения строк
состоит в том, что проверяется их отношение относительно алфавита. Строка "a" "меньше"
строки "b", "bb" "больше" строки "ba". Если обе строки равны - метод возвращает "0", если
первая строка меньше второй – "-1", если первая больше второй – "1".
Чтобы игнорировать регистр букв, в метод нужно передать, как третий аргумент
true.
String.Compare("ab", "Ab"); // возвращает -1
String.Compare("ab", "Ab", true); // возвращает 0
Для перевода строки в верхний/нижний регистр используются методы ToUpper() и
ToLower():
string s = "Hello, World";
Console.WriteLine(s.ToUpper()); // выводит "HELLO, WORLD"
Console.WriteLine(s.ToLower()); // выводит "hello, world"
Для проверки содержания подстроки строкой используется метод Contains().
Данный метод принимает один аргумент – подстроку. Возвращает True, если строка
содержит подстроку, в противном случае – False.
Метод IndexOf() возвращает индекс первого символа подстроки, которую
содержит строка. Данный метод принимает один аргумент – подстроку. Если строка не
содержит подстроки, метод возвращает "-1".
Для того, чтобы узнать, начинается/заканчивается ли строка указанной подстрокой,
используются соответственно методы StartsWith() и EndsWith(), которые
возвращают логическое значение.
Метод Insert() используется для вставки подстроки в строку, начиная с указанной
позиции. Данный метод принимает два аргумента – позиция и подстрока.
Метод Remove() принимает один аргумент – позиция, начиная с которой обрезается
строка.
Console.WriteLine(s.Remove(5)); // удаляем все символы, начиная с 5 позиции,
на экран
В метод Remove() можно передать и второй аргумент – количество обрезаемых
символов. Remove(3, 5) – удалит из строки пять символов начиная с 3-го.
Для получения подстроки из строки, начиная с указанной позиции, используется
метод Substring(). Он принимает один аргумент – позиция, с которой будет начинаться
новая подстрока.
В метод Substring(), как в метод Remove() можно передать и второй аргумент –
длина подстроки. Substring (3, 5) – возвратит подстроку длиной в 5 символов
начиная с 3-й позиции строки.
Метод Replace() принимает два аргумента – подстрока, которую нужно заменить
и новая подстрока, на которую будет заменена первая.
Метод ToCharArray() возвращает массив символов указанной строки:
string s = "Hello, World";
char[] array = s.ToCharArray(); // элементы массива – 'H', 'e', 'l', 'l'…
Метод Split() принимает один аргумент - символ, по которому будет разбита
строка. Возвращает массив строк. Пример:
string s = "Arsenal,Milan,Real Madrid,Barcelona";
string[] array = s.Split(','); // элементы массива – "Arsenal", "Milan",
"Real Madrid", "Barcelona"
Класс List<T>
Класс List<T> представляет простейший список однотипных объектов.
Среди его методов можно выделить следующие:
void Add(T item): добавление нового элемента в список
void AddRange(ICollection collection): добавление с список коллекции
или массива
int BinarySearch(T item): бинарный поиск элемента в списке. Если элемент
найден, то метод возвращает индекс этого элемента в коллекции. При этом список должен
быть отсортирован.
int IndexOf(T item): возвращает индекс первого вхождения элемента в списке
void Insert(int index, T item): вставляет элемент item в списке на
позицию index
bool Remove(T item): удаляет элемент item из списка, и если удаление прошло
успешно, то возвращает true
void RemoveAt(int index): удаление элемента по указанному индексу index
void Sort(): сортировка списка
Рассмотрим реализацию списка на примере:
class Program
{
static void Main(string[] args)
{
List<int> numbers = new List<int>() { 1, 2, 3, 45 };
numbers.Add(6); // добавление элемента
numbers.AddRange(new int[] { 7, 8, 9 });
numbers.Insert(0, 666); // вставляем на первое место в списке число 666
numbers.RemoveAt(1); // удаляем второй элемент
foreach (int i in numbers)
{
Console.WriteLine(i);
}
List<Person> people = new List<Person>(3);
people.Add(new Person() { Name = "Том" });
people.Add(new Person() { Name = "Билл" });
foreach (Person p in people)
{
Console.WriteLine(p.Name);
}
Console.ReadLine();
}
}
class Person
{
public string Name { get; set; }
}
Создаются два списка: один для объектов типа int, а другой - для объектов Person. В
первом случае мы выполняем начальную инициализацию списка: List<int> numbers = new
List<int>() { 1, 2, 3, 45 };
Во втором случае мы используем другой конструктор, в который передаем начальную
емкость списка: List<Person> people = new List<Person>(3);. Указание начальной емкости
списка (capacity) позволяет в будущем увеличить производительность и уменьшить
издержки на выделение памяти при добавлении элементов. Также начальную емкость
можно установить с помощью свойства Capacity, которое имеется у класса List.
Пример можно посмотреть здесь:
http://melfis.ru/%D1%87%D1%82%D0%B5%D0%BD%D0%B8%D0%B5-csv%D1%84%D0%B0%D0%B9%D0%BB%D0%B0-c/