Help us understand the problem. What is going on with this article?

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();
        }
    }
}

上記ソースを GitHubで公開しています。


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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした