やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

C#ソースコードツリーの構築について考えてみる

 自動作成するためにツリー構造を再現する。

成果物

前回

 前回は、以下のような間抜けコードを書くハメになった。

// コード生成
public void CreateCode(in IndentedTextWriter tw)
{
    tw.WriteLine("using System;");
    tw.WriteLine();
    tw.WriteLine("namespace MyNamespace");
    tw.WriteLine("{");
    tw.Indent++;
    tw.WriteLine("public sealed class MyClass");
    tw.WriteLine("{");
    tw.Indent++;
    tw.WriteLine("static void Main(string[] args)");
    tw.WriteLine("{");
    tw.Indent++;
    tw.WriteLine(@"Console.WriteLine(""Hello world"");");
    tw.Indent--;
    tw.WriteLine("}");
    tw.Indent--;
    tw.WriteLine("}");
    tw.Indent--;
    tw.WriteLine("}");
}

問題

 問題はDRYに書けていない。特にIndentやブロック文字は定形処理にできるはず。

解決案

 ツリー構造をモデル化できればDRYに書けるはず。たとえば以下。

new CodeTree()
.Add(new CodeNode()
    .Add(new CodeNode()
        .Add(new CodeNode()))
            .Add(new CodeNode())))

 ノードに挿入したいテキストを与える。

new CodeTree()
.Add(new CodeNode("using System;"))
.Add(new CodeNode("namespace MyNamespace")
    .Add(new CodeNode("class MyClass")
        .Add(new CodeNode("static void Main(string[] args)")
            .Add(new CodeNode(@"Console.WriteLine(""Hello world"");"))))

 namespace, classなどを独自の型にすると、一層わかりやすい。ミスも減る。

new CodeTree()
.Add(new UsingCode("System"))
.Add(new NamespaceCode("MyNamespace")
    .Add(new ClassCode("MyClass")
        .Add(new EntryPointCode(hasArgs=True)
            .Add(new CodeNode(@"Console.WriteLine(""Hello world"");"))))

考察

ブロック文

 ブロックの{,}における改行の指定。主に以下の2タイプがある。

  • 改行+インデント+{
  • スペース1個+{

 コードで書くと以下。

class A
{
}
class A {
}

 これを以下のように設定できるようにしたい。

new CodeTree() { BlockStyle=BlockStyle.NewLine }
.Add(new UsingCode("System"))
.Add(new NamespaceCode("MyNamespace")
    .Add(new ClassCode("MyClass")
        .Add(new EntryPointCode(hasArgs=True)
            .Add(new CodeNode(@"Console.WriteLine(""Hello world"");"))))

 ただ、全体で統一してしまうと思い通りに書けない場合が出てしまう。ブロックスタイルは併用できるようにすべき。

new CodeTree() { BlockStyle=BlockStyle.NewLine }
.Add(new UsingCode("System"))
.Add(new NamespaceCode("MyNamespace")
    .Add(new ClassCode("MyClass")
        .Add(new EntryPointCode(hasArgs=True)
            .Add(new IfCodeNode(ExpressionCode(@"true"), BlockStyle=BlockStyle.Space)
                .Add(new CodeNode(@"Console.WriteLine(""Hello if"");"))
            .Add(new ElseCodeNode(BlockStyle=BlockStyle.Space)
                .Add(new CodeNode(@"Console.WriteLine(""Hello else"");"))

 上記は改行するときとスペースのときの2タイプのブロック文を書く意図。期待値は以下。

using System;

namespace MyNamespace
{
    class MyClass
    {
        static void Main(string[] args)
        {
            if (true) {
                Console.WriteLine("Hello if");
            }
            else {
                Console.WriteLine("Hello else");
            }
        }
    }
}

 namespaceclassなどの{,}は改行されている。だが、ifelse{,}は改行されずスペースされている。

 他にも以下のような6タイプがある。{の前後スペース削除も含めるとさらに増える。今回は上記2タイプ以外の細かいものは除外する。

if (true) {
    Console.WriteLine("Hello if");
} else {
    Console.WriteLine("Hello else");
}
if (true) { Console.WriteLine("Hello if"); }
else { Console.WriteLine("Hello else"); }
if (true) { Console.WriteLine("Hello if"); } else { Console.WriteLine("Hello else"); }
if (true)
    Console.WriteLine("Hello if");
if (true) Console.WriteLine("Hello if");
(true) ? "if" : "else";

 そもそもif文は特殊。ブロック文を連続して書くから。3項演算子はその最たるもの。もはやブロックの改行がどうとかいうレベルでない。分岐はポリモーフィズムやswitch式で書きたい。できるだけif文は書きたくない。

 他に共通化できそうなところは以下。

  • block系: IBlock
    • {, }: Block
    • (,): ParameterBlock
    • [,]: IndexBlock
    • <,>: GenericBlock
  • ;
  • 二項演算子
    • =
    • :

 さらに末尾に;がいる場合といらない場合の2種類がある。

 もっと細かくみてみると以下のような種類がある。参考

 文の中に式が含まれている。文の中には

  • ブロック文
    • block
    • namespace
    • class, interface
    • struct
    • enum
    • method(非仮想メソッド)
    • if,else,else-if
    • switch
    • for, foreach
    • while, do-while
    • checked, unchecked
    • lock, unlock
    • using
    • try-catch-finally

 いずれ、どこまでやるか考えねばならない。

セミコロン

 ところで、}の後ろにセミコロン;が必要なときと不要なときがある。;はブロック文のときは不要なのかと思っていたが違う。lambda式なら() => {};だし、switch式のときはa switch => {};。でもif(){}のときは不要。また、インタフェース仮想メソッド宣言のときにも必要。void M();。このあたりのルールがよくわからない。

仮想メソッドと抽象メソッド

 クラスvirtualメソッド、抽象クラスabstractメソッド、インタフェースメソッド(暗黙public virtual)の違い。

メソッド 規定実装の提供 提供先 override 多重継承
class virtual する -
abstract class abstract しない 継承先のみ
interface 暗黙public virtual しない すべて

 abstract classは1つしか継承できない。interfaceは多重継承できる。

インタフェースを考えてみる

  • CodeTree
    • CodeNode
    • IBlock
      • Block
      • ParameterBlock
      • IndexBlock
      • GenericBlock

 ブロックはよく考えると別物だった。でも先頭と末尾に付与する点は共通。間に複数のいくつかをもつことも。ただしブロックの場合はツリーノードなのに対して()などは単一要素。いや、switch式はツリーノードか?

  • Block: 複数行: { 文; 文; 文; ... }
    • FlowBlock: セミコロン区切り: (1,2,3,...), <T1,T2,...>,
      • (値, 値, 値,...);, { 式, 式, 式, ... };
      • KeyValueFlow: { Key = Value, Key = Value, ...}
      • LambdaFlow: a switch => { 0=>;, _=>; }

 複雑そう。まずは簡単にしたい。

 というわけで、とりあえず以下4つがあればいいか。

  • CodeGenerator
  • CodeTree
    • CodeNode
  • Block

所感

 次回、コードを書いてみる。

対象環境

$ uname -a
Linux raspberrypi 4.19.42-v7+ #1218 SMP Tue May 14 00:48:17 BST 2019 armv7l GNU/Linux