awaitの罠?

手元に開発環境がないのでうろ覚えではあるが・・・


awaitの後のコードが実行されないというケースがあった。

Debug.WriteLine("hoge1");
await hoge2();
Debug.WriteLine("hoge3");

というコードで、hoge2は確かに最後まで実行されているのになぜかhoge3が出力されない。

Debug.WriteLine("hoge1");
hoge2().Wait();
Debug.WriteLine("hoge3");

だとhoge3が出力される。


タイミングとしては、アプリ終了時のOnSuspendingから呼び出された時に起きるようだ。
その他のコードから呼び出すとawait使用時でもhoge3は出力される。


えてしてマルチスレッドというものは、フォアグラウンドスレッドが終わるとバックグラウンドスレッドは通知も何もなくいきなり強制終了されたりするもんだけど、もしかしてこれもそのせい?


以前どこかのサイトでawaitの前後は実行スレッドが変わるというのを読んだことがある。
awaitの後のコードはコンパイラによって別のTaskで実行されるようになるんだとか何とか。
であればhoge2を実行するスレッドがなんであれ、hoge3を出力するコードはバックグラウンドスレッドで実行されることになる。


通常の中断状態ではまた違うのだろうが、ユーザーがアプリを明示的に終了させた場合は中断状態から即終了させられる。
おそらくOnSuspending終了後にはOSによってすべてのスレッドが終了させられてしまうんだろう。
OnSuspending自体はフォアグラウンドスレッドで実行されていて、(ある程度の時間が過ぎるまでは)強制終了されることはない。
しかし、その中でバックグラウンドスレッドが起動されるようなことがあると、そのスレッドの状態がどうであろうとOnSuspendingを抜ければプロセスごと終了させられてしまう。
hoge3が出力されないのはそういう理屈なのではなかろうか?


UIスレッドが実行しているコードであれば、UIに関係するコードはUIスレッドでしか実行できないから、awaitを使った場合でもバックグラウンドにはならないような作りになっているそうだが、OnSuspendingはUIスレッドで実行されてるのかな?
だとしても問題のコードはOnSuspendingからいくつかのawait付き呼び出しやらイベント呼び出しを経由しているし、そもそも通常の動作時でも頻繁に呼び出すコードなのでUIスレッドで実行するわけにもいかないような・・・


awaitではなくWait()で待機すれば問題ないような気もするが、awaitは他でもたくさん使っていて、変にawaitとWait()を混在させるとどこかでデッドロックしてしまうのを度々経験してるもんで、あまり使いたくない。今動いてるのはたまたまだという気がしてならない。


そもそも中断時にawaitを使うような時間のかかることをするなって考えもあるが、中断時に必要とされる処理はアプリが使ったリソースの後始末とアプリの状態の保存である。
windows8の場合、リソース関係の処理はフレームワークからして(ほぼ?)すべてawait/asyncで使うようになっているんだからどうしようもない。
時間がかかるからawaitを使ってるのではなく、フレームワークに非同期メソッドしかないから使わざるを得ない。



長々と書いたが、要約すると
・OnSuspendingから(直接間接問わず)await付きメソッドを呼ぶと問題があるかも知れない
・根本的対策:なし
・暫定対策:awaitではなくWait()で待機するようにして、デッドロックが起きないことを祈る
ってこと。


2013/01/10補足記事追加。
イベントハンドラのasync - たっくてっくのーと


2013/01/31さらに補足というか勘違いの修正。
asyncとawaitとtaskとthread - たっくてっくのーと

ListViewで縦と横にアイテムを配置

ListViewにWrapPanelのような配置をさせる方法。

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        xmlns:vm="clr-namespace:WpfApplication1.ViewModel"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.Resources>
        <ResourceDictionary>
            <CollectionViewSource x:Key="DataSource"
                                  Source="{Binding}"
                                  d:Source="{d:DesignInstance Type=vm:ItemList, IsDesignTimeCreatable=True}"/>
            <SolidColorBrush x:Key="ListBorder" Color="#828790"/>
            <!-- 基本、デフォルトのスタイルそのまま -->
            <Style x:Key="ListViewStyle1" TargetType="{x:Type ListView}">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type ListView}">
                            <Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="1" SnapsToDevicePixels="true">
                                <!-- 変えたのはここのHorizontalScrollBarVisibilityとVerticalScrollBarVisibilityのみ -->
                                <!-- リストビューの同名プロパティに連動する -->
                                <ScrollViewer Focusable="false" Padding="{TemplateBinding Padding}" HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}" VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
                                    <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                                </ScrollViewer>
                            </Border>
                            <ControlTemplate.Triggers>
                                <Trigger Property="IsEnabled" Value="false">
                                    <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
                                </Trigger>
                                <MultiTrigger>
                                    <MultiTrigger.Conditions>
                                        <Condition Property="IsGrouping" Value="true"/>
                                        <Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="false"/>
                                    </MultiTrigger.Conditions>
                                    <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
                                </MultiTrigger>
                            </ControlTemplate.Triggers>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </ResourceDictionary>
    </Window.Resources>

    <Grid>
        <ListView x:Name="listView" ItemsSource="{Binding Source={StaticResource DataSource}}" Style="{StaticResource ListViewStyle1}" ScrollViewer.VerticalScrollBarVisibility="Disabled">
            <ListView.ItemsPanel>
                <ItemsPanelTemplate>
                    <!-- HeightまたはWidthをリストビューと連動させる -->
                    <WrapPanel Orientation="Vertical" Height="{Binding Height, ElementName=listView}"/>
                </ItemsPanelTemplate>
            </ListView.ItemsPanel>
        </ListView>
    </Grid>

</Window>

poi。HSSFCell#setAsActiveCellの問題。その2。

先日の記事の修正。


先日の記事の修正ではウィンドウが分割されている場合にアクティブセルが反映されない。
SelectionRecordクラスのfield_1_paneメンバは、そのレコードがどのペイン(分割されたウィンドウ)に属するのか示すらしい。
よって常に3では意味がない。(3というのは分割されている時の左上のペインを示す)


ウィンドウを分割していてもアクティブにできるペインは1つだけであり、アクティブでないペインのSelectionRecordは何を意味してるのかいまいちよくわからない。
分割位置を示していたりアクティブセルを示していたり?


InternalSheetクラスのオリジナルのコードではSelectionRecordが複数あると一番最後のものを_selectionメンバとして参照するようになっているが、この順番はペインの位置で決まっていて必ずしもアクティブなペインに対応するレコードが最後になるとは限らないので、アクティブなペインのレコードを_selectionメンバに持つようにする。
そのためPaneRecordもメンバとして保持する。
また、今の選択範囲にないセルをアクティブにした時に新しく作るSelectionRecordのfield_1_paneメンバを古いものから引き継ぐ。


InternalSheetクラスに下記のメンバを追加。

private PaneRecord _pane = null;

下記のコンストラクタを修正。

private InternalSheet(RecordStream rs) {
  _mergedCellsTable = new MergedCellsTable();
  RowRecordsAggregate rra = null;

  List<RecordBase> records = new ArrayList<RecordBase>(128);
  _records = records; // needed here due to calls to findFirstRecordLocBySid before we're done
  int dimsloc = -1;

  if (rs.peekNextSid() != BOFRecord.sid) {
    throw new RuntimeException("BOF record expected");
  }
  BOFRecord bof = (BOFRecord) rs.getNext();
  if (bof.getType() != BOFRecord.TYPE_WORKSHEET) {
    // TODO - fix junit tests throw new RuntimeException("Bad BOF record type");
  }
  records.add(bof);
  while (rs.hasNext()) {
    int recSid = rs.peekNextSid();

    if ( recSid == CFHeaderRecord.sid ) {
      condFormatting = new ConditionalFormattingTable(rs);
      records.add(condFormatting);
      continue;
    }

    if (recSid == ColumnInfoRecord.sid) {
      _columnInfos = new ColumnInfoRecordsAggregate(rs);
      records.add(_columnInfos);
      continue;
    }
    if ( recSid == DVALRecord.sid) {
      _dataValidityTable = new DataValidityTable(rs);
      records.add(_dataValidityTable);
      continue;
    }

    if (RecordOrderer.isRowBlockRecord(recSid)) {
      //only add the aggregate once
      if (rra != null) {
        throw new RuntimeException("row/cell records found in the wrong place");
      }
      RowBlocksReader rbr = new RowBlocksReader(rs);
      _mergedCellsTable.addRecords(rbr.getLooseMergedCells());
      rra = new RowRecordsAggregate(rbr.getPlainRecordStream(), rbr.getSharedFormulaManager());
      records.add(rra); //only add the aggregate once
      continue;
    }

    if (CustomViewSettingsRecordAggregate.isBeginRecord(recSid)) {
      // This happens three times in test sample file "29982.xls"
      // Also several times in bugzilla samples 46840-23373 and 46840-23374
      records.add(new CustomViewSettingsRecordAggregate(rs));
      continue;
    }

    if (PageSettingsBlock.isComponentRecord(recSid)) {
      if (_psBlock == null) {
        // first PSB record encountered - read all of them:
        _psBlock = new PageSettingsBlock(rs);
        records.add(_psBlock);
      } else {
        // one or more PSB records found after some intervening non-PSB records
        _psBlock.addLateRecords(rs);
      }
      // YK: in some cases records can be moved to the preceding
      // CustomViewSettingsRecordAggregate blocks
      _psBlock.positionRecords(records);
      continue;
    }

    if (WorksheetProtectionBlock.isComponentRecord(recSid)) {
      _protectionBlock.addRecords(rs);
      continue;
    }

    if (recSid == MergeCellsRecord.sid) {
      // when the MergedCellsTable is found in the right place, we expect those records to be contiguous
      _mergedCellsTable.read(rs);
      continue;
    }

    if (recSid == BOFRecord.sid) {
      ChartSubstreamRecordAggregate chartAgg = new ChartSubstreamRecordAggregate(rs);
      if (false) {
        // TODO - would like to keep the chart aggregate packed, but one unit test needs attention
        records.add(chartAgg);
      } else {
        spillAggregate(chartAgg, records);
      }
      continue;
    }

    Record rec = rs.getNext();
    if ( recSid == IndexRecord.sid ) {
      // ignore INDEX record because it is only needed by Excel,
      // and POI always re-calculates its contents
      continue;
    }


    if (recSid == UncalcedRecord.sid) {
      // don't add UncalcedRecord to the list
      _isUncalced = true; // this flag is enough
      continue;
    }

    if (recSid == FeatRecord.sid ||
      recSid == FeatHdrRecord.sid) {
      records.add(rec);
      continue;
    }

    if (recSid == EOFRecord.sid) {
      records.add(rec);
      break;
    }

    if (recSid == DimensionsRecord.sid) {
      // Make a columns aggregate if one hasn't ready been created.
      if (_columnInfos == null) {
        _columnInfos = new ColumnInfoRecordsAggregate();
        records.add(_columnInfos);
      }

      _dimensions    = ( DimensionsRecord ) rec;
      dimsloc = records.size();
    }
    else if (recSid == DefaultColWidthRecord.sid)
    {
      defaultcolwidth = ( DefaultColWidthRecord ) rec;
    }
    else if (recSid == DefaultRowHeightRecord.sid)
    {
      defaultrowheight = ( DefaultRowHeightRecord ) rec;
    }
    else if ( recSid == PrintGridlinesRecord.sid )
    {
      printGridlines = (PrintGridlinesRecord) rec;
    }
    else if ( recSid == GridsetRecord.sid )
    {
      gridset = (GridsetRecord) rec;
    }
    else if ( recSid == SelectionRecord.sid )
    {
      if (_selection == null) {
        _selection = (SelectionRecord) rec;
      } else if (_pane != null && _pane.getActivePane() == ((SelectionRecord) rec).getPane()) {
        _selection = (SelectionRecord) rec;
      }
    }
    else if ( recSid == WindowTwoRecord.sid )
    {
      windowTwo = (WindowTwoRecord) rec;
    }
    else if ( recSid == GutsRecord.sid )
    {
      _gutsRecord = (GutsRecord) rec;
    }
    else if ( recSid == PaneRecord.sid )
    {
      _pane = (PaneRecord) rec;
    }

    records.add(rec);
  }
  if (windowTwo == null) {
    throw new RuntimeException("WINDOW2 was not found");
  }
  if (_dimensions == null) {
    // Excel seems to always write the DIMENSION record, but tolerates when it is not present
    // in all cases Excel (2007) adds the missing DIMENSION record
    if (rra == null) {
      // bug 46206 alludes to files which skip the DIMENSION record
      // when there are no row/cell records.
      // Not clear which application wrote these files.
      rra = new RowRecordsAggregate();
    } else {
      log.log(POILogger.WARN, "DIMENSION record not found even though row/cells present");
      // Not sure if any tools write files like this, but Excel reads them OK
    }
    dimsloc = findFirstRecordLocBySid(WindowTwoRecord.sid);
    _dimensions = rra.createDimensions();
    records.add(dimsloc, _dimensions);
  }
  if (rra == null) {
    rra = new RowRecordsAggregate();
    records.add(dimsloc + 1, rra);
  }
  _rowsAggregate = rra;
  // put merged cells table in the right place (regardless of where the first MergedCellsRecord was found */
  RecordOrderer.addNewSheetRecord(records, _mergedCellsTable);
  RecordOrderer.addNewSheetRecord(records, _protectionBlock);
  if (log.check( POILogger.DEBUG ))
    log.log(POILogger.DEBUG, "sheet createSheet (existing file) exited");
}

下記のメソッドを修正。

public void setActiveCellRow(int row) {
  //shouldn't have a sheet w/o a SelectionRecord, but best to guard anyway
  if (_selection != null) {
    if (_selection.isInRange(row, getActiveCellCol())) {
      _selection.setActiveCellRow(row);
      return;
    }
    SelectionRecord newSelection = new SelectionRecord(row, getActiveCellCol());
    newSelection.setPane(_selection.getPane());
    int index = _records.indexOf(_selection);
    _records.set(index, newSelection);
    _selection = newSelection;
  }
}

public void setActiveCellCol(short col) {
  //shouldn't have a sheet w/o a SelectionRecord, but best to guard anyway
  if (_selection != null) {
    if (_selection.isInRange(getActiveCellRow(), col)) {
      _selection.setActiveCellCol(col);
      return;
    }
    SelectionRecord newSelection = new SelectionRecord(getActiveCellRow(), col);
    newSelection.setPane(_selection.getPane());
    int index = _records.indexOf(_selection);
    _records.set(index, newSelection);
    _selection = newSelection;
  }
}

public void setActiveCell(int row, int col) {
  //shouldn't have a sheet w/o a SelectionRecord, but best to guard anyway
  if (_selection != null) {
    if (_selection.isInRange(row, col)) {
      _selection.setActiveCell(row, col);
      return;
    }
    SelectionRecord newSelection = new SelectionRecord(row, col);
    newSelection.setPane(_selection.getPane());
    int index = _records.indexOf(_selection);
    _records.set(index, newSelection);
    _selection = newSelection;
  }
}

poi。HSSFCell#setAsActiveCellの問題。

poiを使用していて、HSSFCell#setAsActiveCell()が効かないのに気がついた。
ググってみると動かないという書き込みがいくつかあって、本家にもバグ報告がされてるんだかされてないんだか・・・
poiのバージョンは3.8。
excelファイルは古い形式(拡張子がxls)。
対応策が見つからなかったので何が悪いのか調べてみた。


とりあえずHSSFCell#setAsActiveCell()の中身を見てみる。
どうやら選択範囲のデータを持っているのはorg.apache.poi.hssf.record.SelectionRecordというクラスのようだ。
このクラスのfield_2_row_active_cell、field_3_col_active_cellメンバを変更している。


ところがexcelでアクティブセルを変更したファイルを読んでみると、これ以外にもfield_6_refsメンバの内容も違っている。
何回かexcelで選択範囲とアクティブセルを変更して読み込んだ時の内容から、SelectionRecordクラスのメンバの意味を推測すると、
field_1_pane : よくわからない。常に3?
field_2_row_active_cell : アクティブセルの行インデックス。
field_3_col_active_cell : アクティブセルの列インデックス。
field_4_active_cell_ref_index : アクティブセルを含む選択範囲のインデックス。
field_6_refs : 選択範囲(先頭行、最終行、先頭列、最終列)の配列。
のようになっているらしい。


setAsActiveCell()でfield_2_row_active_cell、field_3_col_active_cellしか変更していないということは、選択範囲の外にアクティブセルがあるということになるのか?
だもんでexcelとしては正しくないアクティブセルは無視して選択範囲をアクティブセルとみなしてるということか。


選択範囲にないセルをアクティブにするのなら選択範囲も変えなきゃいかんだろうし、選択範囲内にしてもセルによってはfield_4_active_cell_ref_indexメンバも変えなきゃいかんだろう。


ということで、そこらへんに対応するようにpoiのコードを変更してみる。


まずorg.apache.poi.hssf.record.SelectionRecord。
下記のメソッドを追加。

public boolean isInRange(int row, int col) {
  for (CellRangeAddress8Bit area : field_6_refs) {
    if (area.isInRange(row, col)) {
      return true;
    }
  }
  return false;
}

public void setActiveCell(int row, int col) {
  field_2_row_active_cell = row;
  field_3_col_active_cell = col;
  setActiveCellRef(row, col);
}

public void setActiveCellRef(int row, int col) {
  for (int i = 0; i < field_6_refs.length; i++) {
    if (field_6_refs[i].isInRange(row, col)) {
      field_4_active_cell_ref_index = i;
      return;
    }
  }
}

下記のメソッドを修正

public void setActiveCellRow(int row) {
  field_2_row_active_cell = row;
  setActiveCellRef(row, getActiveCellCol());
}

public void setActiveCellCol(short col) {
  field_3_col_active_cell = col;
  setActiveCellRef(getActiveCellRow(), col);
}

本来なら行と列は別々に指定できるものではないので@Deprecatedにしたい気もする・・・


次にorg.apache.poi.hssf.model.InternalSheet。
下記のメソッドを追加。

public void setActiveCell(int row, int col) {
  //shouldn't have a sheet w/o a SelectionRecord, but best to guard anyway
  if (_selection != null) {
    if (_selection.isInRange(row, col)) {
      _selection.setActiveCell(row, col);
      return;
    }
    // 選択範囲はprivateメンバなので外からいじれない。
    // 特定のセルならともかく、セル範囲を選択するってことを想定してないような・・・
    // とりあえず今は気にしない。
    SelectionRecord newSelection = new SelectionRecord(row, col);
    int index = _records.indexOf(_selection);
    _records.set(index, newSelection);
    _selection = newSelection;
  }
}

下記のメソッドを修正。

public void setActiveCellRow(int row) {
  //shouldn't have a sheet w/o a SelectionRecord, but best to guard anyway
  if (_selection != null) {
    if (_selection.isInRange(row, getActiveCellCol())) {
      _selection.setActiveCellRow(row);
      return;
    }
    SelectionRecord newSelection = new SelectionRecord(row, getActiveCellCol());
    int index = _records.indexOf(_selection);
    _records.set(index, newSelection);
    _selection = newSelection;
  }
}

public void setActiveCellCol(short col) {
  //shouldn't have a sheet w/o a SelectionRecord, but best to guard anyway
  if (_selection != null) {
    if (_selection.isInRange(getActiveCellRow(), col)) {
      _selection.setActiveCellCol(col);
      return;
    }
    SelectionRecord newSelection = new SelectionRecord(getActiveCellRow(), col);
    int index = _records.indexOf(_selection);
    _records.set(index, newSelection);
    _selection = newSelection;
  }
}

本来なら行と列は・・・同上。


次にorg.apache.poi.hssf.usermodel.HSSFCell。
下記のメソッドを修正。

public void setAsActiveCell() {
  int row=_record.getRow();
  short col=_record.getColumn();
  // _sheet.getSheet().setActiveCellRow(row);
  // _sheet.getSheet().setActiveCellCol(col);
  _sheet.getSheet().setActiveCell(row, col);
}

これでHSSFCell#setAsActiveCell()が効くようになった。


2012/10/09追記
poi。HSSFCell#setAsActiveCellの問題。その2。 - たっくてっくのーと

InputStreamReaderEx

標準のInputStreamReaderは内部的にBufferedReaderを使っていて、readメソッドが実際に何バイト読んだのかわからないので、それがわかるものを作ってみた。

public class InputStreamReaderEx extends Reader {

  private InputStream stream;
  private CharsetDecoder decoder;
  private ByteBuffer bb;
  private long byteCount;
  private long charCount;

  public InputStreamReaderEx(InputStream stream) {
    this.stream = stream;
    decoder = Charset.defaultCharset().newDecoder();
    bb = ByteBuffer.allocate(4);
    bb.flip();
  }

  public InputStreamReaderEx(InputStream stream, String encoding)
      throws UnsupportedEncodingException {
    this.stream = stream;
    Charset cs = Charset.forName(encoding);
    if (cs == null) {
      throw new UnsupportedEncodingException(encoding);
    }
    decoder = cs.newDecoder();
    bb = ByteBuffer.allocate(4);
    bb.flip();
  }

  @Override
  public int read(char[] cbuf, int off, int len) throws IOException {
    // In order to handle surrogate pairs, this method requires that
    // the invoker attempt to read at least two characters. Saving the
    // extra character, if any, at a higher level is easier than trying
    // to deal with it here.
    assert (len > 1);

    CharBuffer cb = CharBuffer.wrap(cbuf, off, len);
    if (cb.position() != 0)
      // Ensure that cb[0] == cbuf[off]
    cb = cb.slice();

    boolean eof = false;
    for (;;) {
      CoderResult cr = decoder.decode(bb, cb, eof);
      if (cr.isUnderflow()) {
        if (eof)
          break;
        if (!cb.hasRemaining())
          break;
        if ((cb.position() > 0) && !inReady())
          break; // Block at most once
        int n = readBytes();
        if (n < 0) {
          eof = true;
          if ((cb.position() == 0) && (!bb.hasRemaining()))
            break;
          decoder.reset();
        }
        continue;
      }
      if (cr.isOverflow()) {
        assert cb.position() > 0;
        break;
      }
      cr.throwException();
    }

    if (eof) {
      // ## Need to flush decoder
      decoder.reset();
    }

    charCount += cb.position();

    if (cb.position() == 0) {
      if (eof)
        return -1;
      assert false;
    }
    return cb.position();
  }

  private boolean inReady() {
    try {
      return (((stream != null) && (stream.available() > 0)) || (stream instanceof FileInputStream)); // ##
                                                                                                      // RBC.available()?
    } catch (IOException x) {
      return false;
    }
  }

  private int readBytes() throws IOException {
    bb.compact();
    try {
      // Read from the input stream, and then update the buffer
      int lim = bb.limit();
      int pos = bb.position();
      assert (pos <= lim);
      int rem = (pos <= lim ? lim - pos : 0);
      assert rem > 0;
      int n = stream.read(bb.array(), bb.arrayOffset() + pos, rem);
      if (n < 0)
        return n;
      if (n == 0)
        throw new IOException("Underlying input stream returned zero bytes");
      assert (n <= rem) : "n = " + n + ", rem = " + rem;
      bb.position(pos + n);
      byteCount += rem;
    } finally {
      // Flip even when an IOException is thrown,
      // otherwise the stream will stutter
      bb.flip();
    }

    int rem = bb.remaining();
    assert (rem != 0) : rem;
    return rem;
  }

  @Override
  public void close() throws IOException {
    stream.close();
  }

  public long getByteCount() {
    if (byteCount == 0) {
      return 0;
    }
    // 正確には元ストリームから何バイト読んだかではなく、readメソッドで読み込んだ文字がトータルで何バイト分かを返す
    return byteCount - (bb.capacity() - bb.position());
  }

  public long getCharCount() {
    return charCount;
  }

}

grepコマンドのフラグ -l -L -v

grepコマンドのフラグでいまいち意味のわからない組み合わせ。
vフラグの効果があったりなかったり?

grep -l a *.txt
grep1.txt
grep2.txt
grep -lv a *.txt
grep1.txt
grep2.txt
grep -l aaa *.txt
grep -lv aaa *.txt
grep1.txt
grep2.txt
grep -L a *.txt
grep -Lv a *.txt
grep -L aaa *.txt
grep1.txt
grep2.txt
grep -Lv aaa *.txt