1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Linqで*=?形式の文字列をparseしてクラスのプロパティを後からいぢる

Last updated at Posted at 2017-06-14

はじめに

少しLinqに慣れてきたので、今までゴリゴリと書いていた文字列のパーズをLinqでやってみた。

やりたいこと

  • クラスの一部のプロパティの値を設定ファイルから代入したい
  • 設定ファイルはシロートが直接いぢるため、xmlとかjsonみたいなのはダメ

対象クラス

public class Hoge
{
    public double A{set;get;}
    public double B{set;get;}
}

var Hoge[] = new Hoge[]
{
    new Hoge{A=1.0,B=2.0},
    new Hoge{A=2.0,B=3.0},
}

対象のクラスは、配列状に並んでいて、後述のindexで識別します。
型はdouble固定っす!

設定ファイルのフォーマット

xmlなんて見ただけで寝込んでしまうような人々のために、こんなフォーマットを考えてみる。

[設定ブロック],[設定ブロック],・・・・・
[設定ブロック] : [プロパティ名#インデックス = 数値]
↓こんな感じ

hoge#1=2.3,piyo=1e-3,poyo=1000

(こういうのって、どうやって現すのがいいのか教えてください)

実装方針

こんなフォーマットの文字列を受け取って、最終的には対象クラスのプロパティを「数値」で上書きすることを目指します。

文字列でメンバーにアクセスできる仕組みを整える

まずは、対象クラスに文字列でアクセスできる仕組みを作り込みます。
ここは、みんな大好きRefrectionで行きます。(というか、動的コード生成とかムリっす)
想定される用途では、アプリ立ち上げ時だけ、この仕組みが動けばいいのでパフォーマンスは無視の方向で。

名前でアクセスする仕組みを持つ基底クラス
toki shirazuさんプロパティ名でプロパティにアクセスするを参考にほぼそのまま使わせていただきました。
これを継承しておけば、インデクサでも、Set~,Get~でも文字列でアクセスできるようになります。

namespace Settings
{
	abstract class ConditionBase
	{
		/// <summary>
		/// プロパティとフィールドの値を名前指定で取得する
		/// </summary>
		/// <param name="Name">プロパティかフィールドの名前</param>
		/// <returns>プロパティかフィールドの値</returns>
		public object GetValueByName(string Name)
		{
			var prop = this.GetType().GetProperty(Name);
			if (prop != null)
			{
				return prop.GetValue(this);
			}
			var fld = this.GetType().GetField(Name);
			if (fld != null)
			{
				return fld.GetValue(this);
			}
			return null;
		}

		/// <summary>
		/// プロパティとフィールドに名前指定で代入する
		/// </summary>
		/// <param name="Name">プロパティかフィールドの名前</param>
		/// <returns>プロパティかフィールドの値</returns>
		public void SetValueByName(string Name, object value)
		{
			var prop = this.GetType().GetProperty(Name);
			if (prop != null)
			{
				prop.SetValue(this, value);
                                return;
			}
			var fld = this.GetType().GetField(Name);
			if (fld != null)
			{
				fld.SetValue(this, value);
                                return;
			}
		}

		/// <summary>
		/// プロパティとフィールドの名前指定アクセス用インデクサ
		/// </summary>
		/// <param name="Name">プロパティかフィールドの名前</param>
		/// <returns>プロパティかフィールドの値</returns>
		public object this[string name]
		{
			get
			{
				return GetValueByName(name);
			}
			set
			{
				SetValueByName(name, value);
			}
		}
	}
}

文字列をparseする

この文字列は、シロートの人間様が編集するので、なるべくエラー処理をしたいが、人間の注意力に期待をして、あまりがちがちにはしない。
エラーに対する対応は以下の通り

  • 表示不可能な文字は、Trimしてしまう
  • スペースは、Trimしてしまう
  • 区切り文字は','のみ
  • 設定ブロック内の'='は1個だけ許容(それ以外だったらその設定ブロックごと無視する)
  • 設定ブロック内の'#'は0個から1個のみ許容(それ以外だったらその設定ブロックごと無視する)
  • 設定ブロック内の'#'が0個のときはインデックス=0とする
  • 名前に変な文字列が含まれていても、GetValueByNameで該当名称が見つからない場合はなにもしないので、目くじら立てない
  • インデックスや数値は、*.TryParseで失敗したら、その設定ブロックごと無視する

こんな感じで実装
設定ブロック毎に、情報を整理して詰め込んだConditionChangerのリストを返します。

namespace Settings
{
	public class ConditionChanger
	{
		public string name;
		public int index;
		public double value;
	}
	public static class Parser
	{
		static List<ConditionChanger> KeyValueParser(string src)
		{
           var sw = new System.Diagnostics.Stopwatch();
            sw.Start();
            if (src == null) return new List<ConditionChanger>();
            return string.Concat(
                // コントロール文字列じゃなくて
                src.Where(c => !char.IsControl(c))
                // スペースじゃない
                .Where(c => c != ' ')
                // 文字を集めて再文字列化
                )
            // したものをコンマで区切って
            .Split(',')
            // =がひとつだけ含まれる要素
            .Where(x => x.Count(y => y == '=') == 1)
            // #がひとつ以内だけ含まれる要素
            .Where(x => x.Count(y => y == '#') <= 1)
            // をゴニョgニョする
            .Select(x =>
                {
                    // 要素の文字列を=と#で分割したListを生成する
                    var z = x.Split('=', '#').ToList();
                    // 要素数が3未満
                    // (Whereで=が1個しかない要素しかここまで来ないから#がない場合)
                    // ならまんなかに"0"を挿入
                    if (z.Count < 3) z.Insert(1, "0");
                    int index;
                    double value;
                    // 失敗したらnullを返しといて後でフィルタリング
                    if (!int.TryParse(z[1], out index)) return null;
                    if (!double.TryParse(z[2], out value)) return null;
                    // Listを返す
                    return new ConditionChanger
                    {
                        name = z[0],
                        index = index,
                        value = value
                    };
                }
            )
            // nullじゃないやつ
            .Where(x => x != null)
            // のListを生成
            .ToList();
		}
	}
}

###使い方
こんな感じで使います。

        class Condition : ConditionBase
        {
            public double A { set; get; }
            public double B { set; get; }
        }

        static void Main(string[] args)
        {
            var cnd = new Condition[] {
                new Condition { A = 1.0, B = 2.0 },
                new Condition { A = 10.0, Bf = 20.0},
            };

            string src = "A=100.0,B=0.001";

            var keyvalue = Settings.Parser.KeyValueParser(src);

            foreach (var kv in keyvalue)
            {
                if (kv.index < 0 || kv.index > cnd.Length - 1) continue;
                cnd[kv.index][kv.name] = kv.value;
            }
        }

インデックスの扱いが野暮ったくて、気に入らないところもありますが、やりたいことはできたようです。

今後の課題

  • double固定をなんとかしたい
  • インデックスの扱いをスタイリッシュにしたい
  • 変換失敗の扱いをもっとスタイリッシュにしたい

まとめ

  • Linqおもしろい
  • リフレクションおもしろい
  • シロート相手をすると、頭を使わなきゃいけない
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?