C#
アルゴリズム
C#小品集シリース

C#:自作RingBufferクラスを使い簡易Tailコマンドを書いてみた

More than 1 year has passed since last update.

リングバッファーを使った Tailコマンドを書いてみました。

作成したTailコマンドの使い方

Tail [行数] [ファイル名]

というとても単純なものです。たとえば、sample.txtの最後の30行を表示したい場合は、

Tail 30 sample.txt

のように入力します。 行数を省略した場合は、最後の10行が表示されます。

ファイル名を省略した場合は、 標準入力からの入力となるようにしました。

RingBufferクラス

Tailコマンドを作成するために、リングバッファークラス(RingBuffer)を自作しました。
RingBufferクラスは、リング状になった配列のような構造をしています。格納できるのは、配列と同様、一定の量だけで、それを超えてデータを格納しようとすると、先頭に戻ってデータを格納する構造になっています。

Wikipediaに説明がありますので、リングバッファについての詳しい説明は割愛します。

wikipwdia:リングバッファ

ここで作成した RingBufferクラスは、

public class RingBuffer<T> : IEnumerable<T> {

とジェネリッククラスにしているので、用途にあった型のデータを入れることができます。

また、IEnumerable<T> を実装していますので、foreachで簡単に要素を取り出すことができます。

なお、この実装では、取り出した要素はバッファから削除していますので、同じ要素を複数回バッファーの中から取り出すことはできません。こういった動きをIEnumerable<T>として実装しても良いの?という疑問も頭をよぎりますが、まあ良しとしましょう。

RingBuffer の簡単な使い方を示します。

// バッファサイズ4のRIngBufferを生成
RingBuffer<string> rb = new RingBuffer<string>(4); 
rb.Add("111111");
rb.Add("222222");
rb.Add("333333");
rb.Add("444444");
rb.Add("555555");
rb.Add("666666");
// 最後の4つを取り出す
foreach (var s in rb) {
    Console.WriteLine(s);
}

このコードを実行すると、以下のように最後に追加した4つの文字列が表示されます。

333333
444444
555555
666666

RingBufferクラスのソースコードはこの記事の最後に掲載しています。

Tailコマンドを実装する

コードはこの後に掲載しているので、それと合わせて読んで欲しいのですが、このプログラムの実質的なメイン部分は、DoTailメソッドです。

観ていただければ分かると思いますが、

「ファイルを1行ずつ読み込み、RingBufferに追加していき、
最後まで読み終わったら、RingBufferの中身をforeachで書き出す」

という、とてもシンプルなコードになっています。Mainメソッドでごにょごにょやってるのはコマンドパラメータの解析なので、本質部分ではありません。

シンプルなコードとなったのは、RingBufferクラスの存在が大きいですね。
こういったクラスがなかったら、 かなり複雑なコードを書かないといけなくなってしまいますし、デバッグも大変です。

using Gushwell.Etude;
using System;
using System.IO;

namespace _23TailCommand {
    class Program {
        static void Main(string[] args) {
            int count = 10;
            string filepath = null;
            if (args.Length > 0) {
                if (int.TryParse(args[0], out count) == false) {
                    count = 10;
                    filepath = args[0];
                } else if (args.Length == 2)
                    filepath = args[1];
            }
            DoTail(count, filepath);
        }

        private static void DoTail(int linecount, string filename) {
            using (var tr = string.IsNullOrEmpty(filename) 
                     ? Console.In : new StreamReader(filename)) {
                RingBuffer<string> rb = new RingBuffer<string>(linecount);
                string line;
                while ((line = tr.ReadLine()) != null) {
                    rb.Add(line);
                }
                foreach (var s in rb)
                    Console.WriteLine(s);
            }
        }

    }
}

RingBufferのソースコード

RingBufferクラスは、ある程度汎用性を持たせましたが、 一般的なRingBufferのインターフェースが良くわからないので、僕なりのインターフェースとしました。
他の用途で利用するには、機能不足のところもあるかもしれません。
特に、Get や foreachでデータを取り出した時に、バッファからデータを除去する仕様としましたが、 除去したくない場合もあると思います。そのような場合は、新たなメソッドを追加する必要があります。

一方、Tailコマンドでの利用だけを考えた場合は、オーバースペックの部分があります。
こういった汎用クラスの設計がなかなか難しいものです。

using System;
using System.Collections;
using System.Collections.Generic;

namespace Gushwell.Etude {
    public class RingBuffer<T> : IEnumerable<T> {
        private int _size;
        private T[] _buffer;
        private int _writeIndex = -1;    // 書き終わった位置
        private bool _isFull = false;

        public RingBuffer(int size) {
            _size = size;
            _buffer = new T[size];
            _writeIndex = -1;
        }

        private int NextIndex(int ix) {
            return ++ix % _size;
        }

        private int GetStartIndex() {
            return _isFull ? NextIndex(_writeIndex) : 0;
        }


        public void Add(T value) {
            _writeIndex = NextIndex(_writeIndex);
            if (_writeIndex == _size - 1)
                _isFull = true;
            _buffer[_writeIndex] = value;
        }

        public bool Exists {
            get { return Count > 0; }
        }

        public int Count {
            get { return _isFull ? _size : _writeIndex + 1; }
        }

        public void Clear() {
            _writeIndex = -1;
            _isFull = false;
        }

        public IEnumerator<T> GetEnumerator() {
            var index = GetStartIndex();
            for (int i = 0; i < Count; i++) {
                yield return _buffer[index];
                index = NextIndex(index);
            }
        }
        IEnumerator IEnumerable.GetEnumerator() {
            return this.GetEnumerator();
        }
    }
}

この記事は、Gushwell's C# Programming Pageで公開したものを加筆・修正したものです。