Programming Taskbook


E-mail:

Пароль:

Регистрация пользователя   Восстановление пароля

English

ЮФУ SMBU

Электронный задачник по программированию

©  М. Э. Абрамян (Южный федеральный университет, Университет МГУ-ППИ в Шэньчжэне), 1998–2024

 

Решения | C#, VB.NET, F# | Обработка файлов

PrevNext


Выполнение заданий на обработку файлов

Данная страница содержит подробное описание процесса решения типовой задачи на обработку двоичных файлов с числовой информацией, а также примеры решения задач на обработку строковых и текстовых файлов.

Двоичные файлы с числовой информацией: File48

Особенности выполнения заданий на обработку файлов рассмотрим на примере задания File48.

File48°. Даны три файла целых чисел одинакового размера с именами SA, SB, SC и строка SD. Создать новый файл с именем SD, в котором чередовались бы элементы исходных файлов с одним и тем же номером: A1B1C1, A2B2C2, ... .

Создание программы-заготовки и знакомство с заданием

Напомним, что проект-заготовку для решения задания можно создать с помощью модуля PT4Load. В созданный проект будет входить файл с именем File48; его расширение зависит от выбранного языка: .cs для C#, .vb для VB.NET и .fs для F# Приведем текст этих файлов без начальных директив:

[C#]

public static void Solve()
{
    Task("File48");
    
}

[VB.NET]

Sub Solve()
    Task("File48")
    
End Sub

[F#]

let Solve = pt.Task "File48"

После запуска программы на экране появится окно задачника. На рисунке приведены два варианта представления окна (в режиме с динамической и с фиксированной компоновкой) в случае использования языка C#:


В первой строке раздела исходных данных указаны имена трех исходных файлов (SA, SB и SC) и одного результирующего (SD). В последующих строках раздела исходных данных показано содержимое исходных файлов. Элементы файлов отображаются бирюзовым цветом, чтобы подчеркнуть их отличие от обычных исходных данных (желтого цвета) и комментариев (светло-серого цвета).

В режиме с фиксированной компоновкой для отображения содержимого каждого двоичного файла отводится по одной строке. Поскольку размер файлов, как правило, превышает количество элементов, которое может уместиться на одной экранной строке, в режиме с фиксированной компоновкой предусмотрена возможность прокрутки элементов файла с помощью мыши или клавиатуры.

В режиме с динамической компоновкой на экране отображаются все элементы двоичных файлов, даже если для этого требуется использовать более одной экранной строки; при этом в начале каждой строки указывается порядковый номер первого элемента файла, приведенного в данной строке (элементы нумеруются от 1).

Следует заметить, что при каждом запуске программы с учебным заданием исходные файлы создаются под новыми именами и заполняются новыми данными, а исходные и результирующие файлы, созданные при предыдущем запуске программы, удаляются с диска.

Вернемся к нашей программе, только что запущенной на выполнение. Так как в ней не указаны операторы ввода-вывода, запуск программы считается ознакомительным, проверка решения не производится, а на экране отображается пример верного решения (в нашем случае это числа, которые должны содержаться в результирующем файле при правильном решении задачи).

Ввод исходных данных

Все классы библиотеки .NET Framework, обеспечивающие работу с файлами, определены в пространстве имен System.IO. Для того чтобы при обращении к именам этих классов не требовалось дополнительно указывать имя пространства имен, в начале программы надо поместить следующую директиву:

[C#]

using System.IO;

[VB.NET]

Imports System.IO

[F#]

open System.IO

Добавим фрагмент, позволяющий ввести имена исходных файлов и связать с этими файлами соответствующие файловые потоки. Поскольку мы собираемся работать с четырьмя файлами, удобно предусмотреть массив потоков:

[C#]

public static void Solve()
{
    Task("File48");
    FileStream[] f = new FileStream[4];
    for (int i = 0; i < 3; i++)
        f[i] = new FileStream(GetString(), FileMode.Open);
}

[VB.NET]

Sub Solve()
    Task("File48")
    Dim f(3) As FileStream
    For i = 0 To 2
        f(i) = New FileStream(GetString(), FileMode.Open)
    Next
End Sub

[F#]

let Solve = pt.Task("File48")
let f = [
    for i = 0 to 2 do
        yield new FileStream(pt.GetString(), FileMode.Open)
    ]

Мы намеренно ограничились тремя итерациями цикла, оставив непрочитанным имя результирующего файла. Введенные имена файлов не сохраняются в строковых переменных, а сразу передаются в конструкторы соответствующих потоков, поскольку после создания потоков все остальные действия с файлами в нашей программе будут осуществляться с использованием потоков, без обращения к именам файлов.

Запуск нового варианта программы уже не будет считаться ознакомительным, поскольку в программе выполняется ввод исходных данных. Так как имя результирующего файла осталось непрочитанным, этот вариант решения будет признан неверным и приведет к сообщению «Введены не все требуемые исходные данные. Количество прочитанных данных: 3 (из 4)».

Пример программы, приводящей к ошибке времени выполнения

Изменим программу так, чтобы в цикле выполнялось не 3, а 4 итерации:

[C#]

    for (int i = 0; i < 4; i++)

[VB.NET]

    For i = 0 To 3

[F#]

    for i = 0 to 3

Теперь запуск программы приведет к сообщению об ошибке. Для языков C# и VB.NET сообщение будет иметь вид «Error System.IO.FileNotFoundException: Could not find file "C:\PT4Work\d8kf.tst"» (имя файла, естественно, будет другим), для языка F# сообщение будет начинаться с текста «Error TypeInitializationException». Сообщение, начинающееся со слова «Error», означает, что при работе программы произошла ошибка времени выполнения (runtime error). В программах, разработанных для платформы .NET, любая ошибка времени выполнения приводит к возбуждению исключения (exception). В тексте сообщения вначале указывается тип возникшего исключения (в данном случае FileNotFoundException или TypeInitializationException), а затем — его краткое описание.

Создание пустого результирующего файла

Чтобы избежать ошибки времени выполнения, отсутствующий файл результатов следует открыть не в режиме FileMode.Open, а в режиме FileMode.Create, что обеспечит создание данного файла. Для этого восстановим прежнее количество итераций цикла, равное 3, а после цикла добавим оператор, открывающий последний, четвертый файловый поток в режиме создания:

[C#]

    f[3] = new FileStream(GetString(), FileMode.Create);

[VB.NET]

    f(3) = New FileStream(GetString(), FileMode.Create)

[F#]

let f = [
    for i = 0 to 2 do
        yield new FileStream(pt.GetString(), FileMode.Open)
    yield new FileStream(pt.GetString(), FileMode.Create)
    ]

В решении на языке F# мы воспользовались генератором списков, где в цикле создаем первые три потока, а вне его — четвертый.

Запуск этого варианта программы не приведет к ошибке времени выполнения, однако будет выведено сообщение «Результирующий файл не закрыт». Для закрытия файловых потоков предусмотрен специальный метод Close, который следует вызывать сразу после того, как работа с данным потоком закончена. Заметим, что если при выполнении заданий с использованием задачника Programming Taskbook результирующие файловые потоки не закрыты методом Close, то решение будет считаться ошибочным (с указанной выше диагностикой).

В нашем случае вызов метода Close удобно организовать в цикле. Внеся это добавление, мы получим следующий вариант решения:

[C#]

public static void Solve()
{
    Task("File48");
    FileStream[] f = new FileStream[4];
    for (int i = 0; i < 3; i++)
        f[i] = new FileStream(GetString(), FileMode.Open);
    f[3] = new FileStream(GetString(), FileMode.Create);
    //
    for (int i = 0; i < 4; i++)
        f[i].Close();
}

[VB.NET]

Sub Solve()
    Task("File48")
    Dim f(3) As FileStream
    For i = 0 To 2
        f(i) = New FileStream(GetString(), FileMode.Open)
    Next
    f(3) = New FileStream(GetString(), FileMode.Create)
    REM
    For i = 0 To 3
        f(i).Close()
    Next
End Sub

[F#]

let Solve = pt.Task "File48"
let f = [ for i = 0 to 2 do 
              yield new FileStream(pt.GetString(), FileMode.Open)
          yield new FileStream(pt.GetString(), FileMode.Create)
        ]
//
for i = 0 to 3 do
    f.[i].Close()

Комментарий (// для C# и F#, REM для VB.NET) расположен в том месте программы, в котором можно выполнять операции ввода-вывода для всех четырех файлов: они уже открыты в режиме Open или Create и еще не закрыты методом Close.

При выполнении этого варианта программы результирующий файл будет создан, однако останется пустым, т. е. не содержащим ни одного элемента. В этом случае на информационной панели выводится сообщение (на светло-синем фоне): «Запуск с правильным вводом данных: все требуемые данные введены, результирующий файл является пустым» (в ранних версиях задачника в этой ситуации на информационной панели выводилось сообщение «Ошибочное решение», а в строке, которая должна содержать элементы результирующего файла, отображался текст EOF — «конец файла», End Of File).

Таким образом, нам осталось реализовать фрагмент алгоритма, обеспечивающий ввод и вывод файловых данных.

Использование неправильных типов для файловых элементов

Во всех ранее рассмотренных вариантах программы мы не использовали операции ввода-вывода для файлов. Поэтому тип файловых элементов нас не интересовал.

Однако при чтении данных из файла (и при их записи в файл) крайне важно правильно указывать тип файловых элементов. Чтобы продемонстрировать это на примере нашей программы, попытаемся прочесть из исходных файлов по одному элементу вещественного типа и запишем прочитанные элементы в файл результатов. Для этого заменим комментарий (// или REM) на следующий фрагмент:

[C#]

    BinaryReader[] r = new BinaryReader[3];
    for (int i = 0; i < 3; i++)
        r[i] = new BinaryReader(f[i]);
    BinaryWriter w = new BinaryWriter(f[3]);
    for (int i = 0; i < 3; i++)
        w.Write(r[i].ReadDouble());

[VB.NET]

    Dim r(2) As BinaryReader
    For i = 0 To 2
        r(i) = New BinaryReader(f(i))
    Next
    Dim w = New BinaryWriter(f(3))
    For i = 0 To 2
        w.Write(r(i).ReadDouble())
    Next

[F#]

let r = [
    for i = 0 to 2 do
        yield new BinaryReader(f.[i])
    ]
let w = new BinaryWriter(f.[3])
for i = 0 to 2 do
    w.Write(r.[i].ReadDouble())

Данный фрагмент создает вспомогательные потоки-оболочки типа BinaryReader и BinaryWriter, обеспечивающие считывание из файла и запись в файл элементов различных типов, после чего производит считывание одного вещественного элемента для каждого из трех исходных файлов и запись этих элементов в результирующий файл (в требуемом порядке). Подчеркнем, что мы неправильно выбрали метод чтения элементов (ReadDouble); тем не менее, компиляция программы пройдет успешно, а после ее запуска не произойдет ошибок времени выполнения.

Результат работы программы будет неожиданным: судя по экранной строке с содержимым результирующего файла, в него будут записаны не три, а шесть элементов, по два начальных элемента из каждого исходного файла. Объясняется это тем, что считывание из файла и последующая запись в файл одного «вещественного элемента» фактически приводит к считыванию и записи блока данных размером 8 байтов, содержащего два последовательных целочисленных элемента исходного файла.

Итак, мы выяснили, что ошибки, связанные с несоответствием типов файловых элементов, не выявляются при компиляции и не всегда приводят к ошибкам времени выполнения. Это следует иметь в виду, и при появлении «странных» результирующих данных начинать поиск ошибки с проверки того, соответствуют ли используемые методы ввода-вывода типу файловых элементов.

В нашем случае для исправления ошибки достаточно заменить метод ReadDouble на ReadInt32 (данный метод обеспечивает считывание из файла элемента целого типа). После запуска исправленной программы мы получим все еще неверный, но вполне «понятный» результат: созданный файл будет содержать три элемента, совпадающих с начальными элементами исходных файлов.

Правильное решение, его тестирование и просмотр результатов

Приведем, наконец, верное решение задачи File48:

[C#]

public static void Solve()
{
    Task("File48");
    FileStream[] f = new FileStream[4];
    for (int i = 0; i < 3; i++)
        f[i] = new FileStream(GetString(), FileMode.Open);
    f[3] = new FileStream(GetString(), FileMode.Create);
    BinaryReader[] r = new BinaryReader[3];
    for (int i = 0; i < 3; i++)
        r[i] = new BinaryReader(f[i]);
    BinaryWriter w = new BinaryWriter(f[3]);
    while (f[0].Position < f[0].Length)
        for (int i = 0; i < 3; i++)
            w.Write(r[i].ReadInt32());
    for (int i = 0; i < 3; i++)
        r[i].Close();
    w.Close();
}

[VB.NET]

Sub Solve()
    Task("File48")
    Dim f(3) As FileStream
    For i = 0 To 2
        f(i) = New FileStream(GetString(), FileMode.Open)
    Next
    f(3) = New FileStream(GetString(), FileMode.Create)
    Dim r(2) As BinaryReader
    For i = 0 To 2
        r(i) = New BinaryReader(f(i))
    Next
    Dim w = New BinaryWriter(f(3))
    Do While f(0).Position < f(0).Length
        For i = 0 To 2
            w.Write(r(i).ReadInt32())
        Next
    Loop
    For i = 0 To 2
        r(i).Close()
    Next
    w.Close()
End Sub

[F#]

let Solve = pt.Task "File48"
let f = [ for i = 0 to 2 do 
              yield new FileStream(pt.GetString(), FileMode.Open)
          yield new FileStream(pt.GetString(), FileMode.Create)
        ]
let r = [
    for i = 0 to 2 do
        yield new BinaryReader(f.[i])
    ]
let w = new BinaryWriter(f.[3])
while f.[0].Position < f.[0].Length do
    for i = 0 to 2 do
        w.Write(r.[i].ReadInt32())
for i = 0 to 2 do
    r.[i].Close()
w.Close()

От предыдущего варианта данное решение отличается добавлением цикла while (Do While для языка VB.NET), условие которого будет истинным, пока в первом файле будут оставаться непрочитанные элементы. Таким образом, цикл while обеспечивает считывание всех элементов из исходных файлов (напомним, что по условию задания все исходные файлы имеют одинаковый размер) и запись их в результирующий файл в нужном порядке. В программе, кроме того, изменен фрагмент, связанный с закрытием файловых потоков: вместо вызова метода Close объектов FileStream выполняется вызов одноименных методов объектов BinaryReader и BinaryWriter. Это обеспечивает закрытие всех потоков, так как при вызове метода Close для потока-оболочки BinaryReader или BinaryWriter происходит автоматический вызов метода Close для связанного с ним базового файлового потока FileStream.

После запуска этого варианта программы и успешного прохождения 5 тестов мы получим сообщение «Задание выполнено!». Нажав клавишу [F2], мы можем вывести на экран окно результатов, в котором будут перечислены все наши попытки решения задания. Приведем содержимое окна результатов в случае решения задания на языке C#:

File48     S24/03 12:15 Ознакомительный запуск.
File48     S24/03 12:16 Введены не все требуемые исходные данные.
File48     S24/03 12:17 Результирующий файл не закрыт.
File48     S24/03 12:18 Error FileNotFoundException.
File48     S24/03 12:20 Запуск с правильным вводом данных.
File48     S24/03 12:22 Ошибочное решение.--2
File48     S24/03 12:24 Задание выполнено!

Примечание. Программу, решающую данное задание, можно сделать более краткой, если не использовать массив объектов FileStream, а создавать эти объекты «на лету», сразу передавая их в конструкторы объектов BinaryReader и BinaryWriter. Кроме того, для создания файловых потоков вместо конструкторов класса FileStream можно использовать методы класса File. Приведем соответствующий вариант решения.

[C#]

public static void Solve()
{
    Task("File48");
    BinaryReader[] r = new BinaryReader[3];
    for (int i = 0; i < r.Length; i++)
        r[i] = new BinaryReader(File.OpenRead(GetString()));
    BinaryWriter
        w = new BinaryWriter(File.Create(GetString()));
    while (r[0].BaseStream.Position < r[0].BaseStream.Length)
        for (int i = 0; i < r.Length; i++)
            w.Write(r[i].ReadInt32());
    foreach (BinaryReader a in r)
        a.Close();
    w.Close();
}

[VB.NET]

Sub Solve()
    Task("File48")
    Dim r(2) As BinaryReader
    For i = 0 To r.Length - 1
        r(i) = New BinaryReader(File.OpenRead(GetString()))
    Next
    Dim w = New BinaryWriter(File.Create(GetString()))
    Do While r(0).BaseStream.Position < r(0).BaseStream.Length
        For i = 0 To r.Length - 1
            w.Write(r(i).ReadInt32())
        Next
    Loop
    For Each a In r
        a.Close()
    Next
    w.Close()
End Sub

[F#]

let Solve = pt.Task "File48"
let r = [
    for i = 0 to 2 do
        yield new BinaryReader(File.OpenRead(pt.GetString()))
    ]
let w = new BinaryWriter(File.Create(pt.GetString()))
while r.[0].BaseStream.Position < r.[0].BaseStream.Length do
    for i = 0 to r.Length - 1 do
        w.Write(r.[i].ReadInt32())
for e in r do
    e.Close()
w.Close()

Обратите внимание на использование во втором цикле for свойства Length массива r вместо константы 2, что делает программу более наглядной и гарантирует, что выхода за границы массива r не произойдет. В заголовке цикла while было использовано свойство BaseStream потока-оболочки BinaryReader; это свойство обеспечивает доступ к свойствам Position и Length файлового потока, связанного с данным потоком-оболочкой.

Обратите также внимание на применение специального цикла foreach (For Each для языка VB.NET, for ... in для языка F#) при вызове метода Close для каждого элемента массива r. Данный цикл обеспечивает большую наглядность по сравнению с циклом for. Однако в цикле foreach элементы массива доступны только для чтения, поэтому цикл foreach нельзя использовать для инициализации элементов массива.


Строковые и текстовые файлы: File67, Text21

В данном пункте описываются особенности выполнения заданий на обработку строковых файлов (т. е. двоичных файлов, содержащих строковые данные), а также текстовых файлов, содержащих строки различной длины, оканчивающиеся маркерами конца строки.

Двоичные строковые файлы

В качестве примера задания на строковые файлы рассмотрим задание File67.

File67°. Дан строковый файл, содержащий даты в формате «день/месяц/год», причем под день и месяц отводится по две позиции, а под год — четыре (например, «16/04/2001»). Создать два файла целых чисел, первый из которых содержит значения дней, а второй — значения месяцев для дат из исходного строкового файла (в том же порядке).

При выполнении заданий на языках C#, VB.NET и F# предполагается, что элементы в двоичном строковом файле всегда имеют одинаковую длину, равную 80 символам (строки меньшей длины дополняются справа пробелами). Это обеспечивает возможность прямого доступа к файловой строке по ее номеру, однако требует удаления завершающих пробелов после считывания строки из файла (для удаления завершающих пробелов можно использовать метод TrimEnd() класса String). Соответственно, перед записью строки в двоичный файл необходимо дополнить ее справа пробелами до 80 символов (для этого достаточно использовать метод PadRight(80) класса String). Помимо символов в строковом файле также хранится информация о длине каждой строки. В случае строки длины 80 эта информация кодируется 1 байтом, поэтому размер каждого элемента в строковом файле в данном случае будет равен 81 байту. Таким образом, если при выполнении задания требуется определить количество элементов в строковом файле, то достаточно разделить размер файла (в байтах) на 81.

Следует подчеркнуть, что условие на длину элементов строковых файлов накладывается задачником Programming Taskbook; в обычных программах, не связанных с выполнением учебных заданий, в строковые файлы можно записывать строки любой длины.

При выполнении заданий на обработку строковых файлов, содержащих русские буквы, необходимо устанавливать для них формат кодирования System.Text.Encoding.Default (данный формат соответствует кодировке ANSI, используемой системой Windows по умолчанию). Формат кодирования указывается в качестве последнего параметра в конструкторах потоков BinaryReader и BinaryWriter. Если этого не сделать, то строки с русскими буквами будут неверно считываться из файлов (и записываться в них).

Примечание. При решении задач в среде Visual Studio Code вместо параметра Encoding.Default необходимо указывать параметр Encoding.GetEncoding(1251). Среда Visual Studio Code доступна для использования, начиная с версии 4.21 базового варианта задачника.

В задании File67 строки, содержащиеся в исходном строковом файле, включают только цифры и символы «/», поэтому устанавливать формат кодирования System.Text.Encoding.Default для данного файла не требуется. Кроме того, при обработке этих строк нет необходимости удалять из них завершающие пробелы. Сразу приведем правильное решение задачи File67, учитывающее отмеченные выше особенности строковых файлов (предполагается, что в программе, решающей задание, с помощью директивы uses для C#, Imports для VB.NET или open для F# указано пространство имен System.IO, как и при решении задания File48):

[C#]

public static void Solve()
{
    Task("File67");
    BinaryReader
        f = new BinaryReader(File.OpenRead(GetString()));
    BinaryWriter
        f1 = new BinaryWriter(File.Create(GetString())),
        f2 = new BinaryWriter(File.Create(GetString()));
    for (long i = 1; i <= f.BaseStream.Length / 81; i++)
    {
        string s = f.ReadString();
        f1.Write(int.Parse(s.Substring(0, 2)));
        f2.Write(int.Parse(s.Substring(3, 2)));
    }
    f.Close();
    f1.Close();
    f2.Close();
}

[VB.NET]

Sub Solve()
    Task("File67")
    Dim f = New BinaryReader(File.OpenRead(GetString())), _
        f1 = New BinaryWriter(File.Create(GetString())), _
        f2 = New BinaryWriter(File.Create(GetString()))
    For i = 1L To f.BaseStream.Length \ 81
        Dim s = f.ReadString()
        f1.Write(Integer.Parse(s.Substring(0, 2)))
        f2.Write(Integer.Parse(s.Substring(3, 2)))
    Next
    f.Close()
    f1.Close()
    f2.Close()
End Sub

[F#]

let Solve = pt.Task "File67"
let f = new BinaryReader(File.OpenRead(pt.GetString()))
let f1 = new BinaryWriter(File.Create(pt.GetString()))
let f2 = new BinaryWriter(File.Create(pt.GetString()))
for i in 1L .. f.BaseStream.Length / 81L do
    let s = f.ReadString()
    f1.Write(int(s.[0..1]))
    f2.Write(int(s.[3..4]))
f.Close()
f1.Close()
f2.Close()

В данной программе для перебора всех строк, содержащихся в исходном файле, использован не цикл while (как в примере решения задания File48), а цикл for, число итераций которого равно количеству строк, содержащихся в файле. Для нахождения этого количества свойство f.BaseStream.Length, равное размеру исходного файлового потока в байтах, делится на 81, т. е. на размер элемента строкового файла (80 символов плюс байт с информацией о длине строки). Поскольку свойство Length имеет тип «длинное целое» (long в C#, Long в VB.NET, int64 в F#), в решении для языка C# указан именно такой тип параметра цикла for, в решении для языка VB.NET тип параметра цикла выводится из типа инициализирующей константы 1L (типа Long), в решении для языка F# использован цикл for ... in для требуемого диапазона длинных целых. Заметим, что для языка F# тоже можно использовать цикл for ... to, если привести его начальное и конечное значение к типу int: for i = 1 to int(f.BaseStream.Length / 81L) do.

Текстовые файлы

В качестве примера задания на текстовые файлы рассмотрим задание Text21.

Text21°. Дан текстовый файл, содержащий более трех строк. Удалить из него последние три строки.

Поскольку текстовые файлы, в отличие от двоичных файлов, нельзя открыть одновременно на чтение и на запись, для изменения текстового файла необходимо воспользоваться вспомогательным файлом. Во вспомогательный файл записываются необходимые результирующие данные, после чего исходный файл удаляется с диска, а имя вспомогательного файла заменяется на имя исходного.

Далее, поскольку строки, содержащиеся в текстовом файле, могут иметь различную длину, для определения числа строк необходимо последовательно считать из файла все его строки. Заметим, что для чтения данных из текстовых файлов следует использовать не класс BinaryReader (как для двоичных строковых файлов), а класс StreamReader. Аналогично, для записи данных в текстовый файл надо использовать класс StreamWriter. При создании этих текстовых потоков не требуется указывать базовый файловый поток типа FileStream; достаточно указать имя файла и (для StreamWriter) дополнительный параметр логического типа, определяющий режим открытия файла: значение false означает режим перезаписи (прежнее содержимое файла пропадает), а значение true — режим дополнения (прежнее содержимое файла дополняется новыми данными). Если файл, указанный в конструкторе потока StreamWriter, не существует, то он автоматически создается, и в этой ситуации режим его открытия может быть любым.

При выполнении заданий на текстовые файлы необходимо указывать формат кодирования System.Text.Encoding.Default (или, при использовании среды Visual Studio Code, формат Encoding.GetEncoding(1251)), поскольку в большинстве заданий исходные текстовые файлы содержат текст на русском языке (данный формат указывается в качестве последнего параметра в конструкторах классов StreamReader и StreamWriter).

Приведем решение задачи Text21, учитывающее отмеченные выше особенности текстовых файлов (предполагается, что в начале программы указаны директивы using / Import / open с пространствами имен System.IO и System.Text):

[C#]

public static void Solve()
{
    Task("Text21");
    string s1 = GetString(), s2 = "$T21$.tmp";
    int n = 0;
    StreamReader f1 = new StreamReader(s1, Encoding.Default);
    StreamWriter f2 = new StreamWriter(s2, false,
        Encoding.Default);
    // Вариант двух предыдущих операторов для среды VS Code:
    // -------------------------------------------------------------------
    // StreamReader f1 = new StreamReader(s1, Encoding.GetEncoding(1251));
    // StreamWriter f2 = new StreamWriter(s2, false,
    //     Encoding.GetEncoding(1251));
    // -------------------------------------------------------------------
    while (!f1.EndOfStream)
    {
        f1.ReadLine();
        n++;
    }
    f1.BaseStream.Position = 0;
    for (int i=0; i < n - 3; i++)
        f2.WriteLine(f1.ReadLine());
    f1.Close();
    f2.Close();
    File.Delete(s1);
    File.Move(s2, s1);
}

[VB.NET]

Sub Solve()
    Task("Text21")
    Dim s1 = GetString(), _
        s2 = "$T21$.tmp", n = 0, _
        f1 = New StreamReader(s1, Encoding.Default), _
        f2 = New StreamWriter(s2, False, Encoding.Default)
    Do While Not f1.EndOfStream
        f1.ReadLine()
        n += 1
    Loop
    f1.BaseStream.Position = 0
    For i = 1 To n - 3
        f2.WriteLine(f1.ReadLine())
    Next
    f1.Close()
    f2.Close()
    File.Delete(s1)
    File.Move(s2, s1)
End Sub

[F#]

let Solve = pt.Task "Text21"
let s1 = pt.GetString()
let s2 = "$T21$.tmp"
let mutable n = 0
let f1 = new StreamReader(s1, Encoding.Default)
let f2 = new StreamWriter(s2, false, Encoding.Default)
while not f1.EndOfStream do
    ignore(f1.ReadLine())
    n <- n + 1
f1.BaseStream.Position <- 0L
for i = 0 to n - 4 do
    f2.WriteLine(f1.ReadLine())
f1.Close();
f2.Close();
File.Delete(s1)
File.Move(s2, s1)

Для определения того, достигнут ли конец текстового файла, проще всего использовать логическое свойство EndOfStream. Следует также обратить внимание на оператор f1.BaseStream.Position = 0 (для языка F# он имеет вид f1.BaseStream.Position <- 0L). Этот оператор обеспечивает переход на начало текстового потока f1 перед повторным считыванием его данных.

В программе на языке F# мы использовали в первом цикле выражение ignore, чтобы подчеркнуть, что значение прочитанной из файла строки должно игнорироваться (при отсутствии выражения ignore компилятор выдаст предупреждение).

Приведенный выше вариант решения является неэффективным, поскольку требует двух просмотров исходного файла f1: первый — для определения его размера, который записывается в переменную n, второй — для создания вспомогательного файла f2, содержащего все строки исходного файла, кроме трех последних.

Задание Text21 можно выполнить и за один просмотр исходного файла, если воспользоваться следующим наблюдением: строка должна быть записана во вспомогательный файл, если после нее в исходном файле находятся по крайней мере три строки. Таким образом, записывать очередную строку во вспомогательный файл следует только после считывания из исходного файла трех следующих за ней строк. Благодаря такому упреждающему считыванию необходимость в предварительном определении размера исходного файла отпадает. Для хранения строк, которые уже считаны из исходного файла, но еще не записаны во вспомогательный файл, удобно использовать массив из трех элементов строкового типа.

Приведем программу, реализующую описанный выше эффективный однопроходный алгоритм решения задания:

[C#]

public static void Solve()
{
    Task("Text21");
    string[] s = new String[3];
    string s1 = GetString(), s2 = "$T21$.tmp";
    int n = 0;
    StreamReader f1 = new StreamReader(s1, Encoding.Default);
    StreamWriter f2 = new StreamWriter(s2, false,
        Encoding.Default);
    // Вариант двух предыдущих операторов для среды VS Code:
    // -------------------------------------------------------------------
    // StreamReader f1 = new StreamReader(s1, Encoding.GetEncoding(1251));
    // StreamWriter f2 = new StreamWriter(s2, false,
    //     Encoding.GetEncoding(1251));
    // -------------------------------------------------------------------
    for (int i= 0; i < 3; i++)
        s[i] = f1.ReadLine();
    while (!f1.EndOfStream)
    {
        f2.WriteLine(s[n]);
        s[n] = f1.ReadLine();
        n = (n + 1) % 3;
    }
    f1.Close();
    f2.Close();
    File.Delete(s1);
    File.Move(s2, s1);
}

[VB.NET]

Sub Solve()
    Task("Text21")
    Dim s(2) As String, s1 = GetString(), _
        s2 = "$T21$.tmp", n = 0, _
        f1 = New StreamReader(s1, Encoding.Default), _
        f2 = New StreamWriter(s2, False, Encoding.Default)
    For i = 0 To 2
        s(i) = f1.ReadLine()
    Next
    Do While Not f1.EndOfStream
        f2.WriteLine(s(n))
        s(n) = f1.ReadLine()
        n = (n + 1) Mod 3
    Loop
    f1.Close()
    f2.Close()
    File.Delete(s1)
    File.Move(s2, s1)
End Sub

[F#]

let Solve = pt.Task "Text21"
let s1 = pt.GetString()
let s2 = "$T21$.tmp"
let mutable n = 0
let f1 = new StreamReader(s1, Encoding.Default)
let f2 = new StreamWriter(s2, false, Encoding.Default)
let s = [| for i in 1 .. 3 -> f1.ReadLine() |]
while not f1.EndOfStream do
    f2.WriteLine(s.[n])
    s.[n] <- f1.ReadLine()
    n <- (n + 1) % 3
f1.Close()
f2.Close()
File.Delete(s1)
File.Move(s2, s1)

В программе на языке F# для создания массива s из трех первых строк исходного файла мы использовали синтаксис генератора массива.


PrevNext

 

Рейтинг@Mail.ru

Разработка сайта:
М. Э. Абрамян, В. Н. Брагилевский

Последнее обновление:
01.01.2024