20210429のC#に関する記事は8件です。

プログラム入門者向け練習問題(回答例)

はじめに この記事は、以下の記事で紹介した初期研修を終えて現場に配属された新人たちに課している練習問題の出題意図や回答例を紹介するものです。 練習問題は特定の言語に依存しないよう作成したつもりですが、私が主にC#の開発に携わっているので以下の回答例はC#のみ作成しています。 全体的な出題意図 主に初期研修(プログラミング等の集合研修)を終えた新人に対して、プログラミング言語への理解度の確認を行う為に出題しました。 集合研修ではあまり提示された仕様を満たすプログラムを作成する練習をしていないようでしたので、提示された仕様から自由にプログラムを作成する練習としても活用できるよう意図しています。 問題文に提示した仕様は、複数の解釈が残るように敢えて細かい指定を省略しています。 回答者は明示された仕様を確実に満たしつつ、明示されていない事項を自由に定めて良いとすることで能動的なプログラム作成行動を促します。 また、以下の問題は単に解かせるだけではなく、問題への取り組みを通して先輩と新人がコミュニケーションを交わせることを目的の一つとしています。 これを通して、お互いのキャラクター(性格やクセ等)を理解し、今後の指導に役立てる準備となることを期待します。 補足 以下の回答例は、正解ではなく、あくまで例となります。 これが唯一の正しいプログラムということも無く、むしろ仕様を満たすことを目的とした場合には不必要な記述さえあります(例えば、回答例ではクラスを多く設けていますが、その全てがなくても仕様を満たすプログラムが作成できます)。 状況によっても求められるプログラムは変わりますので、「このような書き方も出来る」程度に読み取ってもらえると幸いです。 また、回答例は .NET Framework 4.7.2 で動作確認しています。 第3問を除いて、using句は新規クラス作成時に自動作成されるもので十分ですが、.NET Core の場合はusing句が省略されます。 以下のusing句が必要になる場合があるのでご確認ください。 using System; using System.Collections.Generic; using System.Linq; using System.IO; // 第3問のみ 問題ごとの出題意図・回答例 1. Fizz Buzz 問題 問題 問題文はこちら(再掲) 1 から 100 までの数について、Fizz Buzz 問題 を解くプログラムを作成せよ。 ただし、以下の2通りの方針でそれぞれ作成すること。 (1) 可能な限り、少ない行数となるように工夫すること。 (2) 可能な限り、操作ごとにメソッドを分割し、適切な名称を付与すること。 ※ Fizz Buzz 問題 : 以下の通り 1 から順に番号を出力する ただし、3 の倍数 の場合は "Fizz"、5 の倍数 の場合は "Buzz"、両者の公倍数の場合は "Fizz Buzz" を数字の代わりに出力する 出題意図 Fizz Buzz 問題 は有名ですが、これを解くには様々なアプローチがあるので、回答者から出題者まで楽しめると思い、採用しました。 今回は2通りの方針で作成されることで、同じ問題を複数の切り口で考えさせることを意図しました。 行数を減らす為には3項演算子が有用ですので、これに気付いてもらうことが重要です。 メソッドを分割する為には、仕様をできるだけ細かく処理に落とし込む必要があります。また、適切なメソッド名を付与する為には、処理内容には一言で表せる具体性が必要になります。 これらに気付き、工夫してもらうことを目指しました。 回答例 回答例はこちら FizzBuzz問題(少ない行数) public static void Main(string[] args) { Enumerable.Range(1, 100).ToList().ForEach(i => Console.WriteLine(string.Format("{0} {1}", i % 3 == 0 ? "Fizz" : "", i % 5 == 0 ? "Buzz" : i % 3 == 0 ? "" : i.ToString()).Trim())); } FizzBuzz問題(メソッド分割) public static void Main(string[] args) { // 1 ~ 100 の FizzBuzz配列 を作成 var list = CreateFizzBuzzArray(1, 100); // FizzBuzz問題の解を順に出力する foreach (var fz in list) { Console.WriteLine(fz); } } /// <summary> /// 指定した 開始値 から 終了値 までの整数値を元に FizzBuzzインスタンス を作成し、配列で返す。 /// </summary> /// <param name="min">開始値</param> /// <param name="max">終了値</param> /// <returns>FizzBuzzインスタンス配列</returns> private static FizzBuzz[] CreateFizzBuzzArray(int min, int max) { // min ~ max の連番Sequenceを作成し、新規FizzBuzzインスタンスに射影して配列で返す return Enumerable.Range(min, max - min + 1).Select(i => new FizzBuzz(i)).ToArray(); } /// <summary> /// FizzBuzzクラス /// </summary> private class FizzBuzz { /// <summary> /// 対象とする数値 /// </summary> private readonly int i; /// <summary> /// Fizzを出力する条件を満たす /// </summary> public bool IsFizz => this.i % 3 == 0; /// <summary> /// Buzzを出力する条件を満たす /// </summary> public bool IsBuzz => this.i % 5 == 0; /// <summary> /// 数値を出力する条件を満たす /// </summary> public bool IsNumeric => !(this.IsFizz || this.IsBuzz); /// <summary> /// Fizzを出力する条件を満たす場合 "Fizz"、そうでない場合は空文字列 /// </summary> public string Fizz => this.IsFizz ? "Fizz" : string.Empty; /// <summary> /// Buzzを出力する条件を満たす場合 "Buzz"、そうでない場合は空文字列 /// </summary> public string Buzz => this.IsBuzz ? "Buzz" : string.Empty; /// <summary> /// コンストラクタ /// </summary> /// <param name="i">FizzBuzz問題の対象とする数値</param> public FizzBuzz(int i) { this.i = i; } /// <summary> /// このインスタンスに設定された数値を、それと等価な"FizzBuzz"文字列形式に変換します。 /// </summary> /// <returns>FizzBuzz問題の解</returns> public override string ToString() { if (this.IsNumeric) { return this.i.ToString(); } return $"{this.Fizz} {this.Buzz}".Trim(); } } 2. 選択問題 (1) 素数 問題 問題文はこちら(再掲) 1 から 100 までの数のうち、素数のみ出力するプログラムを作成せよ。 ※ 素数 : 1 より大きく、正の約数が 1 と 自身 のみである 自然数 出題意図 特定の条件を満たす数値だけを抽出させる処理です。 素数をプログラミング的にどのように判断するかを考える練習となります。 また、判定方法は効率性等でいくつもありますので、複数ある解決策からいずれかを選ぶ練習にもなるかと思います。 回答例 回答例はこちら 素数 public static void Main(string[] args) { // 100までの素数を順次出力する foreach (var prime in PrimeNumbers(100)) { Console.WriteLine(prime); } } /// <summary> /// 指定した整数値以下の素数を列挙する。 /// </summary> /// <param name="max">取得する素数の最大値</param> /// <returns>素数</returns> private static IEnumerable<int> PrimeNumbers(int max) { // 2未満の場合は空(素数なし) if (max < 2) { yield break; } // 素数リスト(判定済) var primeList = new List<int>(); // 指定数値が素数であるか判定する関数 bool isPrime(int v) { // 素数で割り切れるか判断 foreach (var prime in primeList) { // 対象数値の平方根未満の素数で割り切れるものが無ければ、素数 if (prime * prime > v) { break; } // 対象数値未満の素数で割り切れたら、素数ではない if (v % prime == 0) { return false; } } // 素数 primeList.Add(v); return true; } // 2 ~ 最大値 までの数値について素数のみ返す foreach (var prime in Enumerable.Range(2, max - 1).Where(isPrime)) { yield return prime; } } (2) うるう年判定 問題 問題文はこちら(再掲) 引数で与えられた 年 が うるう年 であるか判定するプログラムを作成せよ。 ただし、引数に 負数 を与えた場合はエラーとする。 ※ うるう年 : 以下の通り判定すること 西暦年 が 4 で割り切れる場合、うるう年とする ただし、西暦年が 100 で割り切れる場合、うるう年では無い ただし、西暦年が 400 で割り切れる場合、うるう年とする 出題意図 コマンドライン引数に応じた判定を行う処理です。 コマンドライン引数は必ずしもプログラマの意図通りに設定されるとは限らないので、バリデーション処理が必要になります。 適切なバリデーションを実装する練習として提示しました。 また、うるう年の判定は提示した文章はそのままプログラム言語に置き換えることが出来ません("ただし"という文言がある為)。 これをどのようにプログラムに落とし込むかも見どころです。 回答例 回答例はこちら うるう年判定 public static void Main(string[] args) { // 引数チェック if (!Validate(args, out var year)) { return; } // 指定された年がうるう年か判定、結果を出力 if (IsLeapYear(year)) { Console.WriteLine($"{year}年はうるう年です。"); } else { Console.WriteLine($"{year}年はうるう年ではありません。"); } } /// <summary> /// 実行引数のバリデーションを行う /// </summary> /// <param name="args">実行引数</param> /// <param name="year">年(引数から取得)</param> /// <returns>問題が無い場合、True</returns> private static bool Validate(string[] args, out int year) { // 初期化 year = 0; // 引数なし if (args.Length == 0) { Console.WriteLine("引数に 年 を指定してください。"); return false; } // 第1引数チェック(年) if (int.TryParse(args[0], out year)) { if (year <= 0) { Console.WriteLine("年 は 正の整数 で入力してください。"); return false; } } else { Console.WriteLine("年 は 整数 で入力してください。"); return false; } // 問題なし return true; } /// <summary> /// 指定した年がうるう年か判定する /// </summary> /// <param name="year">年</param> /// <returns>うるう年の場合、True</returns> private static bool IsLeapYear(int year) { if (year % 4 > 0) { return false; } if (year % 100 == 0) { if (year % 400 == 0) { return true; } return false; } return true; } (3) 乱数 問題 問題文はこちら(再掲) 以下の要件を満たすプログラムを作成せよ。 01 ~ 43 までの 43個の数字 から 異なる6個 を選択し、出力する 出題意図 乱数を用いた「実行するたびに結果が変わる」典型的な処理となります。 また、異なる数字を複数選択しなくてはならないので、既に選択された数字を何らかの方法で判別しなくてはなりません。 この問題も解決方法がいくつもあるので、そのいずれかを選んでプログラムにする必要があります。 回答例 回答例はこちら 乱数 public static void Main(string[] args) { // 初期化 var numbers = new Numbers(); // Numbersインスタンスからランダムで6個の数値を取得(昇順ソート) var balls = numbers.GetBalls(6).OrderBy(i => i).ToArray(); // 選択された数値をスペース区切りで出力する Console.WriteLine(string.Join(" ", balls.Select(i => i.ToString("00")))); } /// <summary> /// 出力数値管理クラス /// </summary> private class Numbers { /// <summary> /// 管理対象の数値リスト /// </summary> private readonly List<int> balls; /// <summary> /// 乱数ジェネレータ /// </summary> private readonly Random rands; /// <summary> /// コンストラクタ /// </summary> public Numbers() { // リストを初期化し 1 ~ 43 を格納する this.balls = Enumerable.Range(1, 43).ToList(); // 乱数ジェネレータの初期化 this.rands = new Random((int)DateTime.Now.Ticks); } /// <summary> /// 管理されている数値の1つをランダムで取り出す /// </summary> public int Ball => this.GetBall(); /// <summary> /// 管理されている数値の1つをランダムで取り出す /// </summary> /// <returns>数値</returns> public int GetBall() { // 数値を取り出す(リストから除外) int takeout(int i) { var value = this.balls[i]; this.balls.RemoveAt(i); return value; } // 取得するリスト要素のインデックス値を乱数で作成 var index = this.rands.Next(this.balls.Count); // リストから数値を取り出して返す return takeout(index); } /// <summary> /// 管理されている数値をランダムで指定した個数だけ取り出す /// </summary> /// <returns>数値</returns> public IEnumerable<int> GetBalls(int count) { foreach (var _ in Enumerable.Range(0, count)) { yield return this.Ball; } } } (4) 小数を含む計算 問題 問題文はこちら(再掲) 引数で与えられた 元金、年数、年利率 を元に 支払総額、月ごとの支払額 を出力するプログラムを作成せよ。 ただし、以下の条件に従うこと(簡単にするため、支払による元金の減少は考慮しない)。 1年ごとに 元金 に 年利率 をかけた金額を 利息 として算出し、元金 と 利息 の合計を 1円単位に四捨五入した金額 を 翌年の元金 とする(複利法) 最終的な元金と利息の合計を 年数 × 12ヶ月 で割り、1円単位に四捨五入した金額 を 月ごとの支払額 とする 月ごとの支払額 を 年数 × 12ヶ月 でかけた金額を 支払総額 とする 全ての計算は10進数で行い、不要な丸め誤差が発生しないようにすること 出題意図 小数を含む演算において、2進数の浮動小数点数値型では小数部の計算が正確に行えないことを認識してもらうことが目的です。 また、この問題では最終的な計算結果は整数とする為、丸め処理(四捨五入)を適切な時点で実施する必要があります。 これを仕様から正確に読み取り、正しくプログラムに落とし込めることを望みます。 なお、支払による元金の減少を考慮しない仕様としている為、このプログラムが出力する月ごとの支払額は現実よりもかなり高くなります。 直観に反する実行結果に対して、「仕様がこうであるからこの結果は正しい」と判断できるかが重要になります。 回答例 回答例はこちら 小数を含む計算 public static void Main(string[] args) { // 引数チェック if (!Validate(args)) { return; } // 引数をパラメータ化 var param = ToParam(args); // 月ごとの支払額 var monthlyPayment = param.MonthlyPayment; // 支払総額 var totalPayment = monthlyPayment * param.Periods * 12; Console.WriteLine($"支払総額 : {totalPayment,13:#,0}"); Console.WriteLine($"月ごとの支払額 : {monthlyPayment,13:#,0}"); } /// <summary> /// 実行引数のバリデーションを行う /// </summary> /// <param name="args">実行引数</param> /// <returns>問題が無い場合、True</returns> private static bool Validate(string[] args) { // 引数が 3個 でない if (args.Length != 3) { Console.WriteLine("引数に 元金 年数 年利率(%) を指定してください。"); return false; } // 元金 が 整数に変換できない または 負数 if (!int.TryParse(args[0], out var principal) || principal <= 0) { Console.WriteLine("元金 は 正の整数 で入力してください。"); return false; } // 年数 が 整数に変換できない または 負数 if (!int.TryParse(args[1], out var periods) || periods <= 0) { Console.WriteLine("年数 は 正の整数 で入力してください。"); return false; } // 年利率 が 数値に変換できない または 負数 if (!decimal.TryParse(args[2], out var rate) || rate <= decimal.Zero) { Console.WriteLine("年利率 は 正の数値 で入力してください。"); return false; } return true; } /// <summary> /// 実行引数をパラメータに変換する /// </summary> /// <param name="args">実行引数</param> /// <returns>パラメータ</returns> private static Param ToParam(string[] args) { return new Param() { Principal = Math.Floor(decimal.Parse(args[0])), Periods = int.Parse(args[1]), Rate = Math.Round(decimal.Parse(args[2]), 2, MidpointRounding.ToEven) }; } /// <summary> /// パラメータ管理クラス /// </summary> private class Param { /// <summary> /// 元金 /// </summary> public decimal Principal { get; set; } /// <summary> /// 年数 /// </summary> public int Periods { get; set; } /// <summary> /// 年利率 /// </summary> public decimal Rate { get; set; } /// <summary> /// 月ごとの支払額 /// </summary> public decimal MonthlyPayment => this.CalcMonthlyPayment(); /// <summary> /// 元金、年数、年利率 を元に 月ごとの支払額 を複利で計算する /// </summary> /// <returns>月ごとの支払額</returns> private decimal CalcMonthlyPayment() { // 支払額 var payment = this.Principal; // 年数分だけ 支払額 に 年利 を加算 foreach (var _ in Enumerable.Range(0, this.Periods)) { payment += Math.Round(payment * this.Rate / 100m, 0, MidpointRounding.ToEven); } // 月ごとの支払額 を算出 return Math.Round(payment / this.Periods / 12m, 0, MidpointRounding.ToEven); } } 3. 一行掲示板 問題 問題文はこちら(再掲) 以下の要件を満たす「一行掲示板」を作成せよ。 コマンドラインアプリとする。 実行時に引数を与えなかった場合、投稿された書き込みを全てコンソールに出力する。 実行時に引数を 1つ 与えた場合、その内容を 投稿文 として登録する。 登録後、投稿された書き込みを全て出力する。 実行時に引数を 2つ以上 与えた場合、エラーとする。 投稿された書き込みは、以下の書式でコンソールに出力する("△" は半角スペース)。 投稿日時("YYYY/MM/DD HH:MM:SS"形式)△投稿文 投稿された書き込みは、投稿日時の降順でコンソールに出力する(新しい書き込みが上にくるようにする)。 投稿された書き込みは、投稿日時 と 投稿文 をCSVファイルとして保存する。 投稿された書き込みは、投稿の都度CSVファイルの末尾に追記する。 プログラムは適宜クラス、メソッドの分割を行い、それぞれに適切な名称を付与すること。 出題意図 比較的規模が大きめの練習問題として、また、ファイルの入出力に触れることを意図しました。 今回の「一行掲示板」は大きく分けて2つの機能(投稿の書き込み と 投稿の一覧表示)を持っています。 羅列された仕様を適切に2つの機能に分割して考えることが出来るかを確認します。 また、この2つの機能は同じファイルを共有する為、ファイルのインタフェースは共通のものにすることが望ましくなります。 このことに気付けるか、気付けた場合にプログラムとして表現できるかが肝要です。 回答例 回答例はこちら 一行掲示板 using System.IO; // 冒頭に左記を追加する必要がある public static void Main(string[] args) { try { // 実行クラスを取得し、実行 GetExecutable(args).Exec(); } catch (ArgumentOutOfRangeException) { Console.WriteLine("引数が多すぎます。"); return; } } /// <summary> /// 実行クラスを取得する /// </summary> /// <param name="args">実行引数</param> /// <returns>実行クラス</returns> private static IExecutable GetExecutable(string[] args) { switch (args.Length) { case 0: // 引数が指定されない場合、投稿内容を表示 return new ReadBoard(); case 1: // 引数が1個指定された場合、引数の内容を投稿する return new WriteBoard(args[0]); default: break; } throw new ArgumentOutOfRangeException(); } /// <summary> /// 実行可能であることを示すインタフェース /// </summary> interface IExecutable { /// <summary> /// 実行メソッド /// </summary> void Exec(); } /// <summary> /// 掲示板の内容を取得する実行クラス /// </summary> class ReadBoard : IExecutable { /// <summary> /// 実行メソッド /// </summary> public void Exec() { Board.Read(); } } /// <summary> /// 掲示板に投稿を行う実行クラス /// </summary> class WriteBoard : IExecutable { /// <summary> /// 投稿メッセージ /// </summary> private readonly string message; /// <summary> /// コンストラクタ /// </summary> /// <param name="message">投稿メッセージ</param> public WriteBoard(string message) { this.message = message; } /// <summary> /// 実行メソッド /// </summary> public void Exec() { Board.Write(this.message); Board.Read(); } } /// <summary> /// 一行掲示板アプリ /// </summary> public static class Board { /// <summary> /// 投稿内容の記録用ファイル名 /// </summary> private const string LogFileName = @"board.txt"; /// <summary> /// 一行掲示板の投稿内容を新規順に出力する /// </summary> public static void Read() { // 投稿内容リスト var contentList = ReadFile(); // 投稿内容を投稿日時の降順でソートして出力する foreach (var contents in contentList.OrderByDescending(c => c.Date)) { Console.WriteLine($"{contents.Date:yyyy/MM/dd HH:mm:ss} {contents.Message}"); } } /// <summary> /// 記録用ファイルを読み込み、投稿内容を列挙する /// </summary> /// <returns>投稿内容</returns> private static IEnumerable<Contents> ReadFile() { if (!File.Exists(LogFileName)) { // ファイルが未作成の場合:NOP yield break; } using (var reader = new StreamReader(LogFileName)) { // 投稿リストを1行ずつ処理 string line; while ((line = reader.ReadLine()) != null) { // Contents インスタンスに変換して返す yield return Contents.Parse(line); } } } /// <summary> /// 一行掲示板にメッセージを投稿する /// </summary> /// <param name="message">投稿メッセージ</param> public static void Write(string message) { using (var writer = new StreamWriter(LogFileName, true)) { // 投稿内容インスタンスを作成し、ファイル末尾に出力 writer.WriteLine(new Contents(message)); } } } /// <summary> /// 投稿内容管理クラス /// </summary> public class Contents { /// <summary> /// 投稿日時 /// </summary> public DateTime Date { get; private set; } /// <summary> /// 投稿メッセージ /// </summary> public string Message { get; private set; } /// <summary> /// デフォルトコンストラクタ /// </summary> public Contents() { } /// <summary> /// コンストラクタ /// </summary> /// <param name="message">投稿メッセージ</param> public Contents(string message) { this.Date = DateTime.Now; this.Message = message; } /// <summary> /// 記録用ファイルの内容をContentsインスタンスに変換する /// </summary> /// <param name="line">記録用ファイルの内容</param> /// <returns>Contentsインスタンス</returns> public static Contents Parse(string line) { try { var texts = line.Split(new char[] { ',' }, 2); return new Contents { Date = DateTime.Parse(texts[0]), Message = texts[1] }; } catch { throw new FormatException(); } } /// <summary> /// Contentsインスタンスの内容を記録用の文字列表現に置き換える /// </summary> /// <returns>記録用の文字列表現</returns> public override string ToString() { return string.Join(",", this.Date.ToString(@"yyyy/MM/dd HH:mm:ss"), this.Message ); } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

拡張メソッドの意義とは?

初めに C#の拡張メソッドの存在意義が文法だけでは分かりにくいと感じ、調査を行いました。 前提条件 C#の文法はなんとなく把握している 拡張メソッドの存在は知っている 対処したい状況 端的に言うと「本来メソッドが追加できないようなクラス・インターフェイスなどにメソッドを追加したい」状況です 「特定のデータに関連するコードが重複してしまうので、解決したい」という状況はよくあると思います。通常、特定の重複部分をメソッドへの抽出して、新しいクラスを定義することで解決ができます。 しかし、コードの変更・追加ができない場合もあります。具体例をいくつか挙げます。 サードパーティー製のコード 素晴らしい機能を提供してくれるライブラリでも、自分の欲しい機能を持つメソッドだけが、不運にも提供されていないことがまれによくあります。 列挙型 そもそもメソッドが持てません インターフェイス そもそも実装を追加することができません なお、ここで想定しているのは、インターフェイスを実装するクラスの内部実装ではなく、インターフェイスに対する利用者側のコードの重複です。基本的には、インターフェイスの内部実装はクラスごとに異なります。 (※C#8.0からはインターフェイスもデフォルト実装を持てますが、新しめの機能であることもあり、例えばUnityでは、Unity2020.1以前では使えないなど、完全な普及にはもうしばらくかかりそうです。) ​ これらに対していつも同じような処理をしている場合には、オブジェクト指向的な解決が難しくなります。以下では、サードパーティー製のクラスThirdPartyClassに、とある機能DoSomething()を追加したくなったという設定で、その解決策を考えます。 考えられる解決策 コピペ 重複したコードを生み出す手間を省くことはできますが、重複自体をなくすことはできません。いろいろな場所で言われることですが、コピペはやめましょう。 ソースを書き換える サードパーティー製のコードに独自で変更を入れるのは、不可能ではないとしても、かなり辛いことになります。そのライブラリがバージョンアップするたびに自分で書いたコードの保守も必要になるからです。また、string型に対する処理など、クラス自体の動作に変更を加えられない場合もあります。 メソッドの導入 void DoSomething(ThirdpartyClass x){ ​ // 直接メソッドを追加できない何らかの処理を行う } 以上のようなコードを重複が発生しているクラスに書くことで重複を削除できます。 この方法だと、コードの見通しが悪くなってしまいます。 なぜなら、この処理が書かれているのは、DoSomething()で処理するデータとは別のデータに関するメソッドを集めたクラスであり、単に内部的に重複したサードパーティー製のクラスへの処理を書くには、論理的に自然な場所とは言い難いからです。 この問題は、別のクラスに静的メソッドを追加することで緩和できます。 public static class ThirdPartyHealper{ public static void DoSomething(ThirdpartyClass x){ // 直接メソッドを追加できない何らかの処理を行う ​ } } 継承による拡張 public class MyClass : ThirdpartyClass{ ​ public void DoSomething(){ ​ // 直接メソッドを追加できない何らかの処理を行う ​ } } 問題となっているクラスを継承し、欲しい機能をメソッドとして追加することで、オブジェクト指向的に自然に機能を拡張することができます。 この方法をとる場合、既存のコードのすべてのサードパーティー製のクラス名を探して継承したものに置き換えなければならず、多少の面倒を伴います。重複が発生している部分以外に、newしている部分全てで新しいクラスのインスタンスを生成しなければならないからです。 ラッパークラスの作成 問題となっているクラスをprivateフィールドとして持ち、欲しい機能を実装したラッパーを作成することで、処理が書かれている場所を論理的に正しくできます。 class ThirdPartyWrapper{ ​ private ThirdPartyClass wrappingObject; ​ public ThirdPartyWrapper(ThirdPartyClass x){ ​ wrappingObject = x; ​ } ​ // 加えてThirdPartyClassの持っているすべてのpublicなフィールド・メソッドに関する同名の委譲メソッドを記述 ​ public void ThirdPartyClassMethod(){ ​ wrappingObject.ThirdPartyClassMethod(); ​ } ​ public void DoSomething(){ ​ // 直接メソッドを追加できない何らかの処理を行う ​ } } この方法では、継承によって解決した場合に比べてnewしているすべての場所を探す必要はありません。しかし、元クラスで公開されているメソッドとフィールドすべてについて、委譲するメソッドとプロパティを生成しなければならず非常に面倒です。 また、既存のコードをラッパーを使うように変更する必要が生まれます。 拡張メソッドの文法 基本的な文法をコードで説明します。今回の記事の趣旨から外れるので詳しい説明は省きます。 // 拡張メソッドは静的クラスで宣言しなければならない public static class ExtensionMethodClass{ // 静的メソッドの第一引数の型の前にthisをつけると拡張メソッドになる public static void ExtensionMethod(this int number){ } public static void UseExtensionMethod(){ // 次の2つの呼び出しは同じ意味になる ExtensionMethod(5); // あたかもインスタンスメソッドかのように呼び出せる 5.ExtensionMethod(); } } 拡張メソッドによる解決 拡張メソッドは、あたかもインスタンスメソッドであるかのように静的メソッドを呼び出すことができる機能です。 したがって、すでに紹介した静的メソッドによる解決を適用した後、それを拡張メソッドに書き換えるだけ解決できます。 public static class ThirdPartyHealper{ ​ public static void DoSomething(this ThirdpartyClass x){ ​ // 直接メソッドを追加できない何らかの処理を行う ​ } } 拡張メソッドによる解決の利点 既存のコードに影響がない ラッパーによる解決や継承による解決は自分の今まで書いたコードに変更を加えてしまいます。 対して拡張メソッドは、既存のコードに変更を加えずとも、重複が発生している部分だけを改善すれば十分です。 拡張が簡単 ラッパーによる解決はラッパー自体の作成による手間が多く、書くのが面倒ですが、拡張メソッドの場合は本質的なロジック以外のコードは最小限に抑えられています。 メソッドチェーンが可能になる これは拡張メソッドが静的メソッドによる解決よりも優れている点です。C#において拡張メソッドが使われている代表的な存在であるLINQでは、以下のようなメソッドチェーンが書けるように定義されており、処理の流れが分かりやすくなります。Sum()やSelect()は、「IEnumerableインターフェースに対していつも同様の処理をしていることが多いのに、インターフェースにコードが書けない」という、まさに今回の記事の課題を解決するものになります。 intArray.Select(x => 2 * x + 5).Where(x => x < 0).Sum(); これを静的メソッドで行うと以下のようになり、かっこの対応が非常に読みにくいです。 Sum(Where(Select(intArray,x => 2 * x + 5), x => x < 0)); 利用者は静的クラスのことを気にせずにコードを書ける 利用者からすると、静的メソッドによる解決をした場合、どうしてもThirdPartyHealperなどといった本来知らなくていいクラスにアクセスしなければなりません。しかし、拡張メソッドを使えば、(usingなどの必要はあるにせよ、ロジックを書く段階では)拡張メソッドがどこで定義されているかわからなくとも特定の機能を使うことができます。 まとめ 拡張メソッドは、その名前の通り、「本来プログラマが変更することが難しいクラスに、あたかもインスタンスメソッドを追加したような拡張をする」機能だと言えます。 蛇足:可読性の向上 これは今回の趣旨である、重複するコードの排除からは外れるかもしれませんが、単純なfor文に対して拡張メソッドを適用することで、以下のようなコードも書くことができ、可読性の向上に貢献します。 5.Times(()=>{ ​ // 5回繰り返したい処理を書く }); 1.Upto(3,(i)=>{ ​ // ループを3回おこない、ループごとにiに1から3が代入される });
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gtk3プリのComboBoxの表示の簡略化

Gtk3アプリのComboBoxの表示の簡略化 ComboBoxの表示の書き方の課題 CellRendererTextやSetCellDataFuncを毎回書くのが面倒 拡張クラスの中で実行させ、書く行数を減らす using System; using System.Collections.Generic; using Gtk; using UI = Gtk.Builder.ObjectAttribute; namespace ComboBoxGtkApplication { class MainWindow : Window { [UI] private ComboBox _comboBox1 = null; List<Song> songs; public MainWindow() : this(new Builder("MainWindow.glade")) { } private MainWindow(Builder builder) : base(builder.GetRawOwnedObject("MainWindow")) { builder.Autoconnect(this); _mkComboBox(); } void _mkComboBox() { songs = new List<Song>(); songs.Add (new Song ("Dancing DJs vs. Roxette", "Fading Like a Flower")); songs.Add (new Song ("Xaiver", "Give me the night")); songs.Add (new Song ("Daft Punk", "Technologic")); _comboBox1._mkCellRendererText("Artist"); Gtk.ListStore musicListStore = new Gtk.ListStore (typeof (Song)); foreach (Song song in songs) { musicListStore.AppendValues (song); } _comboBox1._mkBinding(); _comboBox1.Model = musicListStore; } } public class Song { public Song (string artist, string title) { this.Artist = artist; this.Title = title; } //リフレクションを有効にするためgetとsetを書く public string Artist { get; set; } public string Title { get; set; } } } 解説 リフレクションを使うため、モデルにgeter seterを書く ComboBoxクラスを拡張クラスにする CellRendererTextExにCellRendererTextを親クラスにした派生クラス(継承)を作る CellRendererTextExの中にbindingPropertyNameにモデルのプロパティを指定する ComboBoxクラスを拡張クラス CellRendererTextを継承クラスにする using System; using System.Reflection; using Gdk; using Gtk; public static class objectExtensions { public static object _performSelector_Property(this object obj, string propertyName) { Type magicType = obj.GetType(); PropertyInfo pi = magicType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly); MethodInfo getMethod = pi.GetGetMethod(); object result = getMethod.Invoke(obj, null); return result; } } namespace Gtk { public partial class CellRendererTextEx : Gtk.CellRendererText { public string BindingPropertyName = ""; } public static partial class ComboxExtensions { static public Gtk.CellRendererText _mkCellRendererText(this ComboBox Combox1 ,string baindingName) { Gtk.CellRendererTextEx CellRendererTextEx1 = new Gtk.CellRendererTextEx(); CellRendererTextEx1.BindingPropertyName = baindingName; Combox1.PackStart(CellRendererTextEx1, true); return CellRendererTextEx1; } static public void _mkBinding(this ComboBox Combox1) { if(Combox1.Cells.Length > 0) { Combox1.SetCellDataFunc(Combox1.Cells[0], new Gtk.CellLayoutDataFunc(_RenderComboDo)); } } static private void _RenderComboDo( Gtk.ICellLayout cell_layout, Gtk.CellRenderer cell, Gtk.ITreeModel model, Gtk.TreeIter iter) { if(!(cell is Gtk.CellRendererTextEx)) { return; } if((cell as Gtk.CellRendererTextEx).BindingPropertyName == "" || (cell as Gtk.CellRendererTextEx).BindingPropertyName == null) { Console.WriteLine("PropertyNameがない"); return; } object song = (object)model.GetValue(iter, 0); object value = song._performSelector_Property((cell as Gtk.CellRendererTextEx).BindingPropertyName); if(cell is Gtk.CellRendererText && (value is String)) { (cell as Gtk.CellRendererText).Text = value as String; } else if(cell is Gtk.CellRendererPixbuf && (value is String)) { (cell as Gtk.CellRendererPixbuf).Pixbuf = new Pixbuf((value as String)); } else if(cell is Gtk.CellRendererToggle && (value is String)) { (cell as Gtk.CellRendererToggle).Active = Convert.ToBoolean((value is String)); } else if(cell is Gtk.CellRendererProgress && (value is String)) { (cell as Gtk.CellRendererProgress).Value = Convert.ToInt32((value is String)); } else if(cell is Gtk.CellRendererPixbuf && (value is byte[])) { (cell as Gtk.CellRendererPixbuf).Pixbuf = new Pixbuf((byte[])value); } else if(cell is Gtk.CellRendererToggle && (value is Boolean)) { (cell as Gtk.CellRendererToggle).Active = (Boolean)value; } else if(cell is Gtk.CellRendererProgress && (value is int)) { (cell as Gtk.CellRendererProgress).Value = (int)value; } } } } 続く
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gtk3アプリComboBoxの表示の簡略化

Gtk3アプリのComboBoxの表示の簡略化 ComboBoxの表示の書き方の課題 CellRendererTextやSetCellDataFuncを毎回書くのが面倒 拡張クラスの中で実行させ、書く行数を減らす gladeファイルにCombBoxを配置します。 using System; using System.Collections.Generic; using Gtk; using UI = Gtk.Builder.ObjectAttribute; namespace ComboBoxGtkApplication { class MainWindow : Window { [UI] private ComboBox _comboBox1 = null; List<Song> songs; public MainWindow() : this(new Builder("MainWindow.glade")) { } private MainWindow(Builder builder) : base(builder.GetRawOwnedObject("MainWindow")) { builder.Autoconnect(this); _mkComboBox(); } void _mkComboBox() { songs = new List<Song>(); songs.Add (new Song ("Dancing DJs vs. Roxette", "Fading Like a Flower")); songs.Add (new Song ("Xaiver", "Give me the night")); songs.Add (new Song ("Daft Punk", "Technologic")); _comboBox1._mkCellRendererText("Artist"); Gtk.ListStore musicListStore = new Gtk.ListStore (typeof (Song)); foreach (Song song in songs) { musicListStore.AppendValues (song); } _comboBox1._mkBinding(); _comboBox1.Model = musicListStore; } } public class Song { public Song (string artist, string title) { this.Artist = artist; this.Title = title; } //リフレクションを有効にするためgetとsetを書く public string Artist { get; set; } public string Title { get; set; } } } 解説 リフレクションを使うため、モデルにgeter seterを書く ComboBoxクラスを拡張クラスにする CellRendererTextExにCellRendererTextを親クラスにした派生クラス(継承)を作る CellRendererTextExの中にbindingPropertyNameにモデルのプロパティを指定する ComboBoxクラスを拡張クラス CellRendererTextを継承クラスにする using System; using System.Reflection; using Gdk; using Gtk; public static class objectExtensions { public static object _performSelector_Property(this object obj, string propertyName) { Type magicType = obj.GetType(); PropertyInfo pi = magicType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly); MethodInfo getMethod = pi.GetGetMethod(); object result = getMethod.Invoke(obj, null); return result; } } namespace Gtk { public partial class CellRendererTextEx : Gtk.CellRendererText { public string BindingPropertyName = ""; } public static partial class ComboxExtensions { static public Gtk.CellRendererText _mkCellRendererText(this ComboBox Combox1 ,string baindingName) { Gtk.CellRendererTextEx CellRendererTextEx1 = new Gtk.CellRendererTextEx(); CellRendererTextEx1.BindingPropertyName = baindingName; Combox1.PackStart(CellRendererTextEx1, true); return CellRendererTextEx1; } static public void _mkBinding(this ComboBox Combox1) { if(Combox1.Cells.Length > 0) { Combox1.SetCellDataFunc(Combox1.Cells[0], new Gtk.CellLayoutDataFunc(_RenderComboDo)); } } static private void _RenderComboDo( Gtk.ICellLayout cell_layout, Gtk.CellRenderer cell, Gtk.ITreeModel model, Gtk.TreeIter iter) { if(!(cell is Gtk.CellRendererTextEx)) { return; } if((cell as Gtk.CellRendererTextEx).BindingPropertyName == "" || (cell as Gtk.CellRendererTextEx).BindingPropertyName == null) { Console.WriteLine("PropertyNameがない"); return; } object song = (object)model.GetValue(iter, 0); object value = song._performSelector_Property((cell as Gtk.CellRendererTextEx).BindingPropertyName); if(cell is Gtk.CellRendererText && (value is String)) { (cell as Gtk.CellRendererText).Text = value as String; } else if(cell is Gtk.CellRendererPixbuf && (value is String)) { (cell as Gtk.CellRendererPixbuf).Pixbuf = new Pixbuf((value as String)); } else if(cell is Gtk.CellRendererToggle && (value is String)) { (cell as Gtk.CellRendererToggle).Active = Convert.ToBoolean((value is String)); } else if(cell is Gtk.CellRendererProgress && (value is String)) { (cell as Gtk.CellRendererProgress).Value = Convert.ToInt32((value is String)); } else if(cell is Gtk.CellRendererPixbuf && (value is byte[])) { (cell as Gtk.CellRendererPixbuf).Pixbuf = new Pixbuf((byte[])value); } else if(cell is Gtk.CellRendererToggle && (value is Boolean)) { (cell as Gtk.CellRendererToggle).Active = (Boolean)value; } else if(cell is Gtk.CellRendererProgress && (value is int)) { (cell as Gtk.CellRendererProgress).Value = (int)value; } } } } 続く
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MessagePipe入門

先日、UniTaskやMagicOnionで有名なCysharpが新ライブラリです。MessagePackじゃありませんよ、MessagePipeです。 https://github.com/Cysharp/MessagePipe 入門と言うほど仰々しいものではありませんが、今回開発に携わらせて頂いた知見を元に、MessagePipelineを紹介していきたいと思います。 MessagePipe is 何 コンセプトはシンプルなもので、イベントに関するプログラミングを柔軟かつハイパフォーマンスに行うというのが主眼です。 似た機能を持つライブラリとして、MediatRなどが挙げられますが、こちらはDIファースト(DIを前提に作られており、MS標準の他にもVContainerやZenjectなどに対応している)を掲げており、また他のCysharpライブラリの例に漏れずUnityの対応も厚い、という強みが挙げられます。 そしてパフォーマンスについてですが、GithubのREADMEの最初に示されているように、Subscriberが8つの状態において、C#の素のeventよりも早いという驚異の結果が出ています。 柔軟にイベントを扱う方法として、他にもRxなどが挙げられますが、こちらは素のイベントを置き換える勢いでよりカジュアルに使っていけそうですね。 セットアップ MessagePipeはDIを前提にして作られているので、使うにはDIコンテナに登録する必要があります。 補足:DIについて (念の為に簡単に説明しておくと、DIは、クラス同士の依存関係を一箇所に集約することで簡潔にし、実装の差し替えを可能にする機能のことです。今回はこれを前提に解説をすすめますが、要望があれば無DIからのハンズオンを記事を書くかもしれません(?)のでコメント欄へお願いします。) using MessagePipe; using Microsoft.Extensions.DependencyInjection; Host.CreateDefaultBuilder() .ConfigureServices((ctx, services) => { services.AddMessagePipe(); // AddMessagePipe(options => { }) for configure options }) 以上!これだけです。よくあるサービス登録と違って、これだけであらゆる型の場合をオープンジェネリクスで登録することができます。便利ですね。ちなみにデフォルトではシングルトンで登録されており、optionsで変更することができます。では早速機能を見ていきましょう! Pub/Sub まずは最もシンプルで最も使いそうなPub/Subから。 MessagePipeを使うと、異なるクラス間でイベントを仲介する処理を素のeventよりも柔軟に書くことができます。(同期/非同期、Key付きイベントなど) 例があると分かりやすいので、例えばチャットアプリを作るとしましょう。 //UIに近いサービスクラス public class MessageService { readonly IPublisher<string> publisher; //DIからインスタンスを受け取る public MessageService(IPublisher<string> publisher) { this.publisher = publisher; } public void Send(string message) { // IPublisher<T>.Publish(T message); this.publisher.Publish(message); } } //例えばMagicOnionのStreamingHubのような、通信の送受信を行うクラス public class MessageHub: Hub, IDisposable { readonly IDisposable disposable; //DIからインスタンスを受け取り、イベントハンドラを登録する public MessageService(ISubscriber<string> subscriber) { var bag = DisposableBag.CreateBuilder(); //後処理が出来るようSubscriptionを登録しておく subscriber.Subscribe(x => BroadcastMessage(x)).AddTo(bag); //Build()でただのIDisposableに変換 this.disposable = bag.Build(); } void BroadcastMessage(string message) { Broadcast("MessageReceive",message); } void IDisposable.Dispose() { disposable.Dispose(); } } フレームワーク中立の妄想コードなので、細部には意味はありません! IPublisher/ISubscriberはDIによって裏で型ごとに繋がっているイメージです。素のeventを使おうとした場合、eventをpublicにして直接参照をしたり、間に仲介役のクラスを使う必要がありクラス同士の結合度合いを高めてしまいますが、MessagePipeでは、DIによって疎結合を保ったまま簡単にクラス間でイベントを渡せるメリットがあります。 もう一つ注目したいのが、DisposableBagです。イベントを登録すると、例えば画面遷移したときなどに、必要のなくなったハンドラは解除する場面が多くあります。素のeventであれば、 messageEvent -= OnMessageReceived; などと、一つずつ解除しなければなりません。また、Subscriberの戻り値自体がIDisposableなので、 //field List<IDisposable> subscriptions = new(); public ctor(...) { this.subscriptions.Add(subscriber.Subscribe(x => SomeMethod(x))); this.subscriptions.Add(subscriber.Subscribe(x => SomeMethod2(x))); } IDisposable.Dispose() { subscriptions.ForEach(Dispose); } などとしてあげることも可能ですが、DisposableBagによって、余計なリストを作らずに、これらを一つにまとめ、一気に解除することが出来るようになるわけです。 キー付きPub/Sub これで、MessagePipeを用いて、Messageのイベントのイベントをやり取り出来るようになりましたね。ただ実用を考えると、他にもやるべきことがあります。 例えば、ある種のイベントをサーバーサイドで、ユーザーID毎に空間を分けて(混ざらないように)イベントのやり取りするためには、これでは問題です。 そんな場合でも、僅かな変更で簡単に対応することが出来ます。そう、MessagePipeならね。 それが、2型引数 IPublisher<TKey,TMessage>, ISubscriber<TKey,TMessage>です。 先程のコードに変更を加えてみましょう //UIに近いサービスクラス public class MessageService { public Guid ID {get; set; } //(userId, message)を想定 readonly IPublisher<Guid,string> publisher; public MessageService(IPublisher<Guid, string> publisher) { this.publisher = publisher; } public void Send(Guid userId, string message) { // IPublisher<TKey,TMessage>.Publish(TKey key, TMessage message); this.publisher.Publish(userId, message); } } //例えばMagicOnionのStreamingHubのような、通信の送受信を行うクラス public class MessageHub: Hub, IDisposable { readonly IDisposable disposable; Guid ID; public MessageService(ISubscriber<Guid, string> subscriber) { var bag = DisposableBag.CreateBuilder(); subscriber.Subscribe(ID, BroadcastMessage).AddTo(bag); this.disposable = bag.Build(); //Build()でただのIDisposableに変換 } void BroadcastMessage(string message) { //userId == idのユーザーにメッセージを送信 Broadcast("MessageReceive",message); } void IDisposable.Dispose() { disposable.Dispose(); } } このように、IPublisherとISubscriberを2型引数バージョンにすることで、キー毎にイベントの伝搬を行えるようになります。 実際にこれを実装しようとすると、結構面倒なことになりますが(実体験)MessagePipeを常用することで、better eventとして、様々な状況にパフォーマンスの心配もなく対応することが出来るようになります。 MessagePipeによるPub/Subの雰囲気はだいぶ掴んで頂けたのではと思いますが、次はもう一つの柔軟性である非同期機能についても見ておきましょう。 Async Pub/Sub 先程のキー付きPub/Subでユーザー毎のメッセージ送信機能は実現出来るかと思いますが、通信が絡んで来る場合、多くの場合ではレスポンス性を高めるために非同期を使うことになるかと思います。 MessagePipeでは、各IPublisher,ISubscriberのAsyncバージョンも用意されています。(IAsyncPublisher,IAsyncSubscriber) 例えば、ネットワークを介してメッセージを送ったあと、成功した場合にロギングをしたいとします。 Async付きのものを使うだけで、イベントハンドラをValueTaskとして非同期に待機することが出来るようになります。 public async ValueTask Send(Guid userId, string message) { // IPublisher<TKey,TMessage>.Publish(TKey key, TMessage message); await this.publisher.PublishAsync(userId, message); //待機する //すべてのハンドラの終了後にロギングする logger.LogDebug("メッセージを送信しました"); } //MessageService.cs //ctor public MessageService(ISubscriber<Guid, string> subscriber) { var bag = DisposableBag.CreateBuilder(); // IAsyncSubscriber<TKey,TMessage>.Subscribe(TKey key, Func<TMessage, CancellationToken ValueTask> handler); subscriber.Subscribe(ID, BroadcastMessage).AddTo(bag); this.disposable = bag.Build(); } async ValueTask BroadcastMessage(string message, CancellationToken ct) { //userId == idのユーザーにメッセージを送信 await Broadcast("MessageReceive",message, ct); } IAsyncPublisherは、IPublisherと同様、普通のPublishメソッドも生やしており、こちらはFire and forget、つまり待機する必要の無い時にvoidで発行するときに使います。 同期と非同期を併用する場合は、Async Pub/Subに統一してしまうと良いかもしれませんね。 Filter I(Async)?SubscriberのSubscribeは、引数にAsyncMessageHandlerFilter<TMessage>[]型としてフィルターを受け付けています。ここにフィルターを渡すと、ハンドラーの実行前後に、任意の処理を挟むことが出来ます。 例えば、先程のロギング処理は、あのように書くのも良いかもしれませんが、関心を分けるためにフィルターとして挿入するもの有用です。 //MessageHandlerFilterは、ジェネリクスのままでも、特定の型のフィルターとしても使えます。 public class AsyncLoggingFilter<T> : AsyncMessageHandlerFilter<T> { readonly ILogger<AsyncLoggingFilter<T>> logger; //FilterにもDI可能! public AsyncLoggingFilter(ILogger<LoggingFilter<T>> logger) { this.logger = logger; } public override ValueTask HandleAsync(T message, CancellationToken ct, Func<string, CancellationToken, ValueTask> next) { try { //前処理 logger.LogDebug("メッセージを送信します"); await next(message, ct); //ハンドラ本体の処理を待機 //後処理 logger.LogDebug("メッセージを送信しました"); } catch (Exception ex) { logger.LogError(ex, "error"); } } } これをSubscribe時に渡すことで、フィルターが有効になります。 subscriber.Subscribe(handler, new AsyncLoggingFilter<string>()); ロギングの場合、個別の場合に登録しなくても動作するのが望ましいと思います。その為に、DIでGlobalに登録すると良いでしょう。 Host.CreateDefaultBuilder() .ConfigureServices((ctx, services) => { services.AddMessagePipe(options => { //任意のMessageHandlerの実行前後に、AsyncL options.AddGlobalMessageHandlerFilter(typeof(AsyncLoggingFilter<string>), -10000 /*優先順位*/); }); }); これで任意のstring型の非同期イベントハンドラの実行前後にロギングするフィルターを登録することが出来ました。 フィルターの登録箇所は、AddGlobalMessageHandlerFilter<TFilter>もありますが、オープンジェネリクスを用いる為に、typeofを使った記法をおすすめします。 他の機能など 以上でPub/Subに関しては一通り説明出来たかと思います。MessagePipeは他にも、RequestHandler によって、Mediatorパターンを実装したり、EventFactoryによって、型に依らずにグルーピングしたりすることが出来ます。 ここまで読んでいただいてなんですが、 CysharpのREADME、@neuecc さんが結構頑張っていらっしゃるので分かりやすいです。情報も更新されていくでしょうし、こちらを読むのをおすすめします。 とは言え、要望や感想などをいただければある程度応えるつもりですのでコメント欄へお願いします。 筆者略歴 初めまして。初Qiitaなので挨拶させてください。普段はC#を書いており、UnityやWebの開発をしたりしていますが、最近必要に駆られてTypescriptとReactを若干覚えました(趣味ですがRustも好きです。最近はぼちぼちVulkanと言語処理系の勉強をしてます) 本当は今頃楽しく大学で青春しているはずだったのですが、最大の学びは現場にある。プログラミングは独学第一!という持論がどうしても強く、現在はCysharpでインターンをさせて頂いております。 普段は面倒で記事を書いたりなど発信はほぼしないのですが、今回は鉄を熱いうちに打つ絶好の機会だったので書かせていただきました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gtk3アプリのTreeViewの表示の簡略化

Gtk3 TreeViewの表示の簡略化 GtkSharpチュートリアルに書かれているサンプルを簡略化します treeViewの表示の書き方の課題 CellRendererTextやSetCellDataFuncを毎回書くのが面倒 拡張クラスの中で実行させ、書く行数を減らす using System; using System.Collections.Generic; using Gtk; using UI = Gtk.Builder.ObjectAttribute; namespace treeView3GtkApplication { class MainWindow : Window { [UI] private TreeView _treeView1 = null; List<Song> songs; public MainWindow() : this(new Builder("MainWindow.glade")) { } private MainWindow(Builder builder) : base(builder.GetRawOwnedObject("MainWindow")) { builder.Autoconnect(this); _mkTreeView(); } void _mkTreeView() { songs = new List<Song>(); songs.Add (new Song ("Dancing DJs vs. Roxette", "Fading Like a Flower")); songs.Add (new Song ("Xaiver", "Give me the night")); songs.Add (new Song ("Daft Punk", "Technologic")); Gtk.TreeViewColumnEx artistColumn = new Gtk.TreeViewColumnEx (); artistColumn.Title = "Artist"; //CellRenderの生成 artistColumn._mkCellRendererText(); //モデルのプロパティを指定する artistColumn.bindingPropertyName = "Artist"; Gtk.TreeViewColumnEx songColumn = new Gtk.TreeViewColumnEx (); songColumn.Title = "Song Title"; songColumn._mkCellRendererText(); songColumn.bindingPropertyName = "Title"; _treeView1.AppendColumn (artistColumn); _treeView1.AppendColumn (songColumn); Gtk.ListStore musicListStore = new Gtk.ListStore (typeof (Song)); foreach (Song song in songs) { musicListStore.AppendValues (song); } _treeView1._mkBinding(); _treeView1.Model = musicListStore; } } public class Song { public Song (string artist, string title) { this.Artist = artist; this.Title = title; } //リフレクションを有効にするためgetとsetを書く public string Artist { get; set; } public string Title { get; set; } } } 解説 リフレクションを使うため、モデルにgeter seterを書く treeViewを拡張クラスにする treeViewColumnにExを追加した派生クラス(継承)をにする bindingPropertyNameにモデルのプロパティを指定する TreeView拡張クラス using System; using System.Reflection; using Gtk; static class GtkExtensions { public static void _mkBinding(this TreeView treeView) { foreach (TreeViewColumnEx column in treeView.Columns){ if(!(column is TreeViewColumnEx)) { return; } TreeViewColumnEx columnt1 = (column as TreeViewColumnEx); columnt1._mkBinding(); } } } TreeViewColumnEx 派生クラス using System; using System.Reflection; using Gtk; using Gdk; public static class objectExtensions { public static object _performSelector_Property(this object obj, string propertyName) { Type magicType = obj.GetType(); PropertyInfo pi = magicType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly); MethodInfo getMethod = pi.GetGetMethod(); object result = getMethod.Invoke(obj, null); return result; } } namespace Gtk { public class TreeViewColumnEx : TreeViewColumn { public String bindingPropertyName = ""; public CellRendererText _mkCellRendererText(string title = "",bool isPackStart = true) { if (title != "") { this.Title = title; } Gtk.CellRendererText CellRendererText1 = new Gtk.CellRendererText(); this.PackStart(CellRendererText1, isPackStart); return CellRendererText1; } public CellRendererPixbuf _mkCellRendererPixbuf(string title = "",bool isPackStart = true) { if (title != "") { this.Title = title; } Gtk.CellRendererPixbuf CellRendererPixbuf1 = new Gtk.CellRendererPixbuf(); this.PackStart(CellRendererPixbuf1, isPackStart); return CellRendererPixbuf1; } public CellRendererToggle _mkCellRendererToggle(string title = "",bool isPackStart = true) { if (title != "") { this.Title = title; } Gtk.CellRendererToggle CellRendererToggle1 = new Gtk.CellRendererToggle(); this.PackStart(CellRendererToggle1, isPackStart); return CellRendererToggle1; } public CellRendererProgress _mkCellRendererProgress(string title = "",bool isPackStart = true) { if (title != "") { this.Title = title; } Gtk.CellRendererProgress CellRendererProgress1 = new Gtk.CellRendererProgress(); this.PackStart(CellRendererProgress1, isPackStart); return CellRendererProgress1; } public void _mkBinding() { if(this.Cells.Length > 0) { this.SetCellDataFunc(this.Cells[0], new Gtk.TreeCellDataFunc(_RenderCellDo)); } } private void _RenderCellDo(Gtk.TreeViewColumn column, Gtk.CellRenderer cell, Gtk.ITreeModel model, Gtk.TreeIter iter) { if(!(column is TreeViewColumnEx)) { return; } TreeViewColumnEx columnt1 = (column as TreeViewColumnEx); if(columnt1.bindingPropertyName == "" || columnt1.bindingPropertyName == null) { Console.WriteLine("PropertyNameがない"); return; } object song = (object)model.GetValue(iter, 0); object value = song._performSelector_Property(columnt1.bindingPropertyName); if ( cell is Gtk.CellRendererText && (value is String)) { (cell as Gtk.CellRendererText).Text = value as String; } else if(cell is Gtk.CellRendererPixbuf && (value is String)) { (cell as Gtk.CellRendererPixbuf).Pixbuf = new Pixbuf(null, (value as String)); } else if(cell is Gtk.CellRendererToggle && (value is String)) { (cell as Gtk.CellRendererToggle).Active = Convert.ToBoolean((value is String)); } else if(cell is Gtk.CellRendererProgress && (value is String)) { (cell as Gtk.CellRendererProgress).Value = Convert.ToInt32((value is String)); } else if(cell is Gtk.CellRendererPixbuf && (value is byte[])) { (cell as Gtk.CellRendererPixbuf).Pixbuf = new Pixbuf((byte[])value); } else if(cell is Gtk.CellRendererToggle && (value is Boolean)) { (cell as Gtk.CellRendererToggle).Active = (Boolean)value; } else if(cell is Gtk.CellRendererProgress && (value is int)) { (cell as Gtk.CellRendererProgress).Value = (int)value; } } } } Gtk3アプリのComboBoxの表示の簡略化に続く
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CsvHelper ver 27 リリース

2021年4月22日に、CsvHelper version 27.0.0 がリリースされました。 主な新機能、変更は次の通りです。 新機能 デリミタを自動判別できるようになりました。デフォルトはOffです。 登録されているすべての型に型コンバーターを適用する機能が追加されました。 登録されているすべてのタイプにタイプコンバータオプションを適用する機能が追加されました。 IAsyncEnumerableをWriteRecordsに渡す機能が追加されました。 変換に失敗したときにデフォルト値を使用するオプションが追加されました。 仕様変更 IParserConfiguration.DetectDelimiterが追加されました。 IParserConfiguration.DetectDelimiterValuesが追加されました。 IWriter.WriteRecordsAsync (IAsyncEnumerable レコード、CancellationToken cancelToken =デフォルト)を追加しました。 デフォルトとしてCsvConfiguration.WhiteSpaceCharactersから「\t」を削除しました。 公式ページはこちら。 ソースコードを見たい方はこちらからどうぞ。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

連想配列、List,Arraylist、キャスト、参照

連想配列は便利だけどメソッドで返せない。 ArraylistかListなら返せる。 Arraylistだと何でもバンバン突っ込めるがObject型なので 明示キャストが必要。あと参照外れる。 Listなら当たり前だけどキャスト要らないし参照も外れない。 テスト時にArraylistで組んでて コードレビュー受けてListにしたらキャストの手間も要らないし 参照も外れないから全件検索の必要なくなるよ ってコードで言われて赤面しました。 あと配列はシャローコピーとディープコピーがあるらしく 一次レイヤーは値コピー、参照コピーの使い分けができるけど 二次レイヤー以下は参照コピー扱いになっちゃって毎回初期化してた。 ここの解決策ほしい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む