14
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#上級者向け】ジェネリック型制約の暗黙知を3分類:データ表示・入力検証・コマンドの実装例

14
Last updated at Posted at 2026-01-03

1921 views(2026年1月4日)

Examples of Using Generic Type Constraints in C# for Data Display, Input Validation, and Command Implementation

For Advanced C# Developers: Classifying Implicit Knowledge of Generic Type Constraints into Three Categories — Data Display, Input Validation, and Command Implementation Examples

投稿後1年経過でいいねされない記事は削除され、非公開領域に移されます。

この記事の対象読者

  • C#中級者〜上級者

  • ジェネリック型・where 制約の設計に悩んでいる人

  • CSV / JSON など異種入力を コンパイル時に制限したい人

  • 「この Entity × このフォーマットはおかしい」をif 文ではなく型で表現したい人

最近の2本足羊に関する投稿

(こういうのも書いています)

いいね3以上🏴:追加済み
ストック3以上:bride_with_veil_tone1::入力検証の実装例を追加
ストック6以上👑:コマンド実装・処理系の実装例を追加

※ 折角、長時間かけて執筆したのに誰も読んでくれないなどという無駄を避けるために、このようにしています。

🎈寄付のお願い🎈
7$以上の寄付🤡(※1):この記事でクレジット表示 + 紹介Link。
記念として巻き角スペルチェッカーの追加機能実装(※2)

※1.約1000円相当です。
※2.巻き角スペルチェッカーのソースコードの公開には累計70ドル以上の寄付が必要です。

GitHub(随時プロジェクト追加)

Visual Studio2026 .net7.0 - .net10

めんどくせ(笑)

Wpf.Prism を使用しています。

Gist

序文

前回の記事 :【C#】Interfaceをデリゲートのように扱って規約実装を伝播させる設計パターンでジェネリック型制約の使い方に不満が残ったので、掘り下げようかと思います。

ジェネリック型制約とはクラス名<T(型パラメータ) > : where Tのwhere部分です。

 


AIによるとジェネリック型制約には[データ表示・入力検証・コマンド/処理系]
の3つの用途があり、これはベテランプログラマの知見から逆算したものです。
→ 検索した限りではそのように表現した記事なりサイトなりが見当たらないので、AI推論を活用した言語化 により表現実装として落とし込むのは有意義であると考え、本稿を執筆致しました。

※但し、かなり苦労させられたが。

c;ass<T> : where:Tの詳しい使い方は以下

参考:https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/where-generic-type-constraint
参考2:制約条件


ベテランはWhere:Tを実際にどう使っているか(AIによる推定)

① データ表示系

※ 実際のところどうなのか知りたいので、有識者によるコメントをお待ちしております。

折りたたみ

ベテランはこう書きます。

class ListViewModel<TEntity> where TEntity : IEntity, INotifyPropertyChanged

この時点で彼らは分かっています。

これは「何でも入るList」ではない表示・更新・通知が前提のデータ群だ

ただし彼らはこれを 「データ表示系用途」とは呼ばない

単に 「このViewModelにはこの制約が必要」 としか言わない。

② 入力/検証系

ここは特に無意識レベルです。

実行時例外で落とすのはダサい、検証できない型は設計ミス
という価値観が先にあって、where T : IValidatable
を書く。

これは完全に身体知としての設計判断

③ コマンド/処理系

ここも経験者ほど嫌悪感が強い領域です。

object parameter(引数) 
(T)parameter
null地獄

これを何度も踏んだ結果、GenericController<TParam>に行き着く。
(筆者注:つまり引数とクラス実装の実態が噛み合っていない)

彼らはこれを「ジェネリック型制約の用途:データ表示」とは呼ばない

単に 「objectは信用しない」 という反射です。

筆者補足: ジェネリック型制約により、明示的にwhere:T実装を指定することで契約の明文化がなされる。

この知見をWPFの実装例として落とし込んでいきます。

:princess_tone2::princess_tone2::princess_tone2:女性とひつじの観点によるジェネリック型制約の説明🐑🐑🐑

※単なる私の趣味なので読み飛ばしてください。

details
  • 女性の観点
    現実でも 「誰でもOKな集まり」 と 「最低限これができる人」
    では、安心感と会話の成立率がまるで違う。

where T : IDisposable「後片付けができる人限定」 みたいなもの。

これが無いとどうなるか。

・片付ける前提で話しているのに
・相手が片付けを知らない
・毎回『できる?』『やったことある?』と確認が必要

結果、疲れる

女性的設計感覚で言うと
「察して」ではなく
「最初から条件を書いておいてほしい」
になる。

  • ひつじの観点からの説明
    ジェネリックの T は、草原に集まってくる「何か」。
    正体不明。角があるかもしれないし、狼かもしれない

羊からすると
「誰でも来ていいよ」
恐怖でしかない。

だから羊は思う。

「せめて
・草を食べる
・夜に吠えない
・急に噛まない
このくらいは保証してほしい」

これが where T : IHerbivore だ。

  • この説明は如何でしたか?
    ※よろしければ以下のアンケートにお答えください。
    回答者は抽選で感謝の言葉を贈ります。

実装

今回の実装ではViewModelの責務を3つに分ける実装を致しました。なかなか悪くないと思っている。

WPF実装例1:汎用データ表示ListView 🖥️

  • 動画

仕様

  • 1.Toggle Buttonを押すとUserEntityLogEntityを切り替えます

  • 2.動的にListViewのViewModelを切り替えます

ToggleButton(UserEntity) = new CsvRepository() → User 用 ViewModel を生成
ToggleButton(LogEntity) = new JsonRepository() → Log 用 ViewModel を生成

  • 3.それぞれのEntityにおいて、defaultのデモ用ユーザーリスト・そのEntityファーマットのCSVファイルそのEntityファーマットのJsonファイルを読み込みます。

  • 3-A. ファイル形式が違うか、フォーマッが指定外のものだと実行時例外を吐きます。(FormatException)

・・・・・・・・・・・・・

これはC#における型システムが範囲外のためですが、型をToken化して型情報そのものに CSVファイルファイルがParseされた というプロセスを付加することでこれを内部実装として拡張できます。

実行用のCSV/Jsonファイル

用意するのを忘れましたので追記します。
コピペしてお手元のローカルストレージへ保存してください。

CSVファイル Clickで展開
1,Merino_lady,Wool Department
2,Suffolk_lady,Meat Research
3,Dorset_lady,Breeding Lab
4,Cheviot_lady,Mountain Team
5,Romney_lady,Grassland Unit
6,Corriedale_lady,Crossbreed Section
7,Lincoln_lady,Longwool Division
8,Southdown_lady,Quality Control
9,Hampshire_lady,Field Operations
10,Texel_lady,Genetics Group
Jsonファイル Clickで展開
[
  {
    "Timestamp": "2026-01-01T08:15:23.1234567+09:00",
    "Level": "Info",
    "Message": "アプリケーションが正常に起動しました。"
  },
  {
    "Timestamp": "2026-01-01T09:02:11.4567890+09:00",
    "Level": "Warning",
    "Message": "データベース接続が一時的に遅延しています。"
  },
  {
    "Timestamp": "2026-01-01T10:30:45.7890123+09:00",
    "Level": "Error",
    "Message": "ユーザー認証に失敗しました。無効なトークンが指定されました。"
  },
  {
    "Timestamp": "2026-01-01T11:12:07.2345678+09:00",
    "Level": "Debug",
    "Message": "リクエスト処理開始: GET /api/users/123"
  },
  {
    "Timestamp": "2026-01-01T12:45:59.8901234+09:00",
    "Level": "Info",
    "Message": "バックアップ処理が正常に完了しました。"
  },
  {
    "Timestamp": "2026-01-01T13:20:33.5678901+09:00",
    "Level": "Error",
    "Message": "ファイルアップロードに失敗しました。ディスク容量不足の可能性があります。"
  },
  {
    "Timestamp": "2026-01-01T14:55:12.3456789+09:00",
    "Level": "Warning",
    "Message": "外部APIのレスポンスタイムが通常より長いです(2500ms)。"
  },
  {
    "Timestamp": "2026-01-01T15:08:44.9012345+09:00",
    "Level": "Info",
    "Message": "ユーザーID: 456 がログインしました。"
  },
  {
    "Timestamp": "2026-01-01T16:37:21.6789012+09:00",
    "Level": "Debug",
    "Message": "キャッシュ更新完了: キー = product:789"
  },
  {
    "Timestamp": "2026-01-01T17:59:58.0123456+09:00",
    "Level": "Info",
    "Message": "日次バッチ処理が終了しました。処理件数: 12500"
  }
]

実装

Entity

LoadFromCsv()はInterfaceの制約上合わせる必要がありました。

日本語訳で 実在、存在、実在物、実体、本体、自主独立体 だそうです
辞書:https://ejje.weblio.jp/content/entity 
そういや昔はWebkioをよく使っていたなぁと

UserEntity

Interface制約を合わせる必要があったんで、ICsvReadableを継承して
LoadFromCSV()は未実装としています。
本当は必要ないので微妙なんですが、仕方なく許容しています。

UserEntity
using GenericTypeConstraintsPatterns.Interface;
using GenericTypeConstraintsPatterns.Repository;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;

namespace GenericTypeConstraintsPatterns.Entity
{

    public class UserEntity : ICsvReadable<UserEntity>, IListItem
    {
        public int Id { get; set; }
        public string DisplayName { get; set; } = string.Empty;
        public string affiliation { get; set; } = string.Empty;

        public ObservableCollection<UserEntity> LoadFromCsv()
        {
            throw new NotImplementedException();
        }
    }
}

LogEntity

LogEntity
using GenericTypeConstraintsPatterns.Interface;


namespace GenericTypeConstraintsPatterns.Entity
{

    /// <summary>
    /* 戻り値型を Interface 側で固定する必要がある

    IJsonReadable<LogEntity> により
    

LoadFromJson() の戻り値が LogEntity で確定
        */

    /// </summary>
        public sealed class LogEntity
           : IJsonReadable<LogEntity>,ILogItem
        {
            public DateTime Timestamp { get; init; } = DateTime.Now;
            public string Level { get; init; } = string.Empty;
            public string Message { get; init; } = string.Empty;

            public IEnumerable<LogEntity> LoadFromJson()
            {
                return new[]
                {
                new LogEntity
                {
                    Timestamp = DateTime.Now,
                    Level = "INFO",
                    Message = "Application started"
                }
            };
            }
        }

}

Interfaceの構成

※主要部分のみ

ICsvReadable.cs

ICsvReadable.cs
using GenericTypeConstraintsPatterns.Entity;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;

namespace GenericTypeConstraintsPatterns.Interface
{

    /// <summary>
    /*自己型制約付き ICsvReadable<TEntity> は

「静的メソッドの戻り値を型安全に固定したい」場合に有効

デメリットは、読者が CRTP を理解する必要があること

つまり「やや上級者向け」だが、Where:T の威力を見せるには最適
    */
    /// </summary>
    /// <typeparam name="TEntity"></typeparam>
    public interface ICsvReadable<TEntity>
+      where TEntity : ICsvReadable<TEntity>  /// 自己型制約
        /*「TEntity は ICsvReadable<TEntity> を実装している必要がある」
            実際には TEntity 自身がこのインターフェイスを実装するのが前提 */
    {
        // CSV から自分自身のコレクションを生成する静的メソッド
        abstract ObservableCollection<TEntity> LoadFromCsv();
    }



IJsonReadable.cs

Jsonファイルが読み込み出来ることを実装上保証します。



namespace GenericTypeConstraintsPatterns.Entity
{
    public interface IJsonReadable<TEntity>     
    {
        IEnumerable<LogEntity> LoadFromJson();
    }
}

IListItem.cs

参照箇所が多いです。
UserEntityに対応しています。

IListItem.cs

namespace GenericTypeConstraintsPatterns.Interface
{
    ///UseEntityに対応
    public interface IListItem
    {
        public int Id { get; set; }
        public string DisplayName { get; set; }

        public string affiliation
        { get; set; }
    }
}

ILogItem.cs

ILogItem.cs
namespace GenericTypeConstraintsPatterns.Interface
{
    public interface ILogItem
    {
        public DateTime Timestamp { get; init; }
        public string Level { get; init; }
        public string Message { get; init; }
    }
}

ファイルごとに分割しておりますが、可読性を考えると隣接させた方がいいかもしれない。

image.png

通常のInterface実装との違いを詳細に解説
public interface ICsvReadable だと

効果:(型パラメータなしで) 単純に「CSV から生成できる」型であることを表現

・反作用
戻り値の型が object 系統になりやすく、型安全性が低下
ViewModel などで「TEntity に対応した CSV」を扱う場合は型キャストが必要

例(Clickで展開) ※理解を深めるため、本稿のCodeに沿って解説させていただきました。
public interface ICsvReadable
{
}

public class UserEntity : ICsvReadable, IListItem
{
}

public class CsvLoader<TEntity>
    where TEntity : class
{
    public IEnumerable<ICsvReadable> CsvLoad(string path)
    {
        // CSV 読み込み処理
        yield break;
    }
}

  • 呼び出し側

var csvloader = new CsvLoader<UserEntity>();

if (_mainViewModel.CurrentEntity is ListViewModel<UserEntity> vm)
{
    vm.Items.Clear();

    foreach (var token in csvloader.CsvLoad(_mainViewModel.FilePath))
    {
        // ↓ ここで型キャストが必要になる
        vm.Items.Add((UserEntity)token);
    }
}

自己型制約(CRPT)について

 public interface ICsvReadable<TEntity>
      where TEntity : ICsvReadable<TEntity>  /// 自己型制約

正式には Curiously Recurring Template Pattern。
本来は C++ のテンプレート手法で、
「派生クラスが自分自身を基底クラスの型引数として渡す」 というものです。

C#において public interface ICsvReadable<TEntity> + where TEntity ICsvReadable<TEntity> は 

必ず 「自分の型を型引数として渡した状態」で実装される ことが保証されます。

自己型制約がない場合(where TEntity : class)

以下折り畳み

型の対応関係が壊れていても、コードは書けてしまう。

public class SheepEntity { }

////// 実装の意味的な破綻
public class UserEntity : ICsvReadable<SheepEntity>, IListItem
{
}

自己型制約がある場合(where TEntity : ICsvReadable)

これは コンパイルエラー になる。


public class SheepEntity { }

public class UserEntity : ICsvReadable<SheepEntity>, IListItem
{
    // ❌ コンパイルエラーで文脈と噛み合わないクラスを
    //型パラメータとして使えない
}

実際にこうなるよ!

image.png

エラー内容:

型 'GenericTypeConstraintsPatterns.Entity.LogEntity' はジェネリック型またはメソッド 'ICsvReadable' 内で型パラメーター 'TEntity' として使用できません。'GenericTypeConstraintsPatterns.Entity.LogEntity' から 'GenericTypeConstraintsPatterns.Interface.ICsvReadable' への暗黙的な参照変換がありません。

Repositoryの構成

Repository とは、
データの取得・保存方法を隠蔽し、利用側に「どうやって読んでいるか」を意識させずに扱わせるためのクラスです。

いわゆる物流工場のようなもので、中でどんな保管方式や読み取り手順が使われているかを利用側に見せず、必要なデータを受け渡す役割だけを担うクラスを指します。

CsvRepository

Code展開
CsvRepository.cs
using GenericTypeConstraintsPatterns.Interface;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Windows;

namespace GenericTypeConstraintsPatterns.Repository
{
    public class CsvRepository<TEntity> : ICsvReadable<TEntity>
        where TEntity : class, ICsvReadable<TEntity>
    {
        readonly string _filePath = string.Empty;
+  readonly Func<string[], TEntity> _factory;
///呼び出しの際にDelegate(匿名メソッド)が必要
        public CsvRepository(string filePath,
+         Func<string[], TEntity> entityFactory)
        {
            _filePath = filePath;
            _factory = entityFactory;
        }




+        public ObservableCollection<TEntity> LoadFromCsv()
        {
            var collection = new ObservableCollection<TEntity>();

            if (string.IsNullOrWhiteSpace(_filePath))
            {
                MessageBox.Show("CSVファイルが選択されていません。", "エラー", MessageBoxButton.OK, MessageBoxImage.Warning);
                return collection;
            }

            foreach (var line in File.ReadLines(_filePath))
            {
                var columns = line.Split(',');
                TEntity entity;
                try
                {
                    entity = _factory(columns);
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message, "Error parsing CSV line", MessageBoxButton.OK, MessageBoxImage.Error);
                    break;
                }

                collection.Add(entity); // 追加ごとに UI に通知される
                int rowIndex = 0;
                foreach (var csvline in File.ReadLines(_filePath))
                {
                    rowIndex++;
                    var testcolumns = line.Split(',');
                    Debug.WriteLine($"Row {rowIndex}, ColumnCount: {testcolumns.Length}");
                }
            }

+           return collection;
+       /// collection要素だけを返す
        }
    }
}

JsonRepository

Code展開
JsonRepository.cs


using GenericTypeConstraintsPatterns.Interface;
using System.IO;
using System.Text.Json;

namespace GenericTypeConstraintsPatterns.Repository
{

    /*
     * この設計の強みは以下です:共通の保存・読み込みロジックを1箇所にまとめられるUserEntity 用、LogEntity 用と別々に JsonRepository を作らなくて済む
JSONファイルへのシリアライズ/デシリアライズ、ファイルパス管理、エラーハンドリングなどが全部共通化

型安全が保たれるwhere TEntity : class, IListItem のおかげで、IListItem を実装していない型はコンパイルエラーになる
誤って関係ないクラスをリポジトリに渡すミスを防げる


     */
    public class JsonRepository<TEntity> 
        where TEntity : class, ILogItem
    {
        private readonly string _filePath;
        readonly Func<string[], TEntity>? _factory;
        public JsonRepository(string filePath)
        {
            _filePath = filePath;
        }

       public JsonRepository(string filePath, Func<string[], TEntity> entityFactory)
        {
            _filePath = filePath;
            _factory = entityFactory;
        }

        public IEnumerable<TEntity> LoadAll()
        {
            try
            {
                var json = File.ReadAllText(_filePath);
                return JsonSerializer.Deserialize<List<TEntity>>(json)
                     
                    ?? Enumerable.Empty<TEntity>();
            }

            catch (Exception ex)
            {

                MessageBoxService.ShowError(ex, "Error reading JSON file");
                return Enumerable.Empty<TEntity>();
            }
        }
    }

}

ViewModelの構成

Wpd.Prismを使用しています。
 https://www.nuget.org/packages/Prism.Wpf

ListViewModel

ListViewの表示用Modelとして
EnTityType、EnTityTypeの型情報、通知用のItems を持ちます。

以下折りたたみ
ListViewModel.cs
using GenericTypeConstraintsPatterns.Interface;
using Prism.Common;
using System.Collections.ObjectModel;

namespace GenericTypeConstraintsPatterns.ViewModel
    {

        public class ListViewModel<TEntity> : ObservableObject<TEntity>,IListViewModel
               where TEntity : class, new() // new() を追加すると Add コマンドで便利
    {


            ///// コレクション自体は固定(1つだけ作る)にして、内容をクリア&追加
+        public Type CurrentEntityType => typeof(TEntity);

+        public ObservableCollection<object> Items { get; } = new ObservableCollection<object>();


+        Type IListViewModel.CurrentEntityType { get => CurrentEntityType; set => throw new NotImplementedException(); }





        /// <summary>
        /// ListViewに通知するためのコレクション
        /// </summary>
        /// <param name="entities"></param>
        public ListViewModel(IEnumerable<TEntity>? entities = null)
            {
                Items = new ObservableCollection<object>();

                if (entities != null)
                {
                    foreach (var entity in entities)
                    {
                        Items.Add(entity);
                    }
                }
            }


        }



        /*
      T ではなく TEntity
    where TEntity : IListItem で意図が一発で分かる
    MVVM 寄りだが責務は「表示用データ保持」だけ

        */
    }



ViewModelCommand

Commmandを持たせたクラス

public MainViewModel _mainViewModel { init; get; } を持つ。

以下折りたたみ
ViewModelCommand.cs

using GenericTypeConstraintsPatterns.Entity;
using GenericTypeConstraintsPatterns.Interface;
using GenericTypeConstraintsPatterns.Loader;
using GenericTypeConstraintsPatterns.Repository;
using Microsoft.Win32;
using System.Diagnostics;
using System.Text.Json;
using System.Windows;
using System.Windows.Input;

namespace GenericTypeConstraintsPatterns.ViewModel
{

    /// <summary>
    /// MainViewModelから制御されるコマンド群
    /// </summary>
    public class ViewModelCommand : BindableBase
    {

        /// <summary>
        /// DelegateCommandはPrismのICommand実装
        /// なので実務上は抽象型を渡すのが正しい
        /// 汎用性を持たせるため
        /// </summary>
        public ICommand ShowInMemoryCommand { get; }
        public ICommand ShowCsvCommand { get; }
        public ICommand ShowJsonCommand { get; }
        public ICommand FilePickerCommand { get; set; }

        public ICommand ToggleEntityCommand { get; }


+        public MainViewModel _mainViewModel { init; get; }

        ListViewModel<LogEntity> logEntity;
        ListViewModel<UserEntity> userEntity;

        public ViewModelCommand(MainViewModel mainViewModel)
        {
            _mainViewModel = mainViewModel;

            _mainViewModel.FilePath = string.Empty;
            ShowInMemoryCommand = new DelegateCommand(ShowInMemory);
            ShowCsvCommand = new DelegateCommand(ReadCsv<UserEntity>);
            ShowJsonCommand = new DelegateCommand(ShowJson<LogEntity>);
            FilePickerCommand = new DelegateCommand(FilePatheSelect);

             logEntity = new();
             userEntity = new();


            ToggleEntityCommand = new DelegateCommand(() =>
            {
                if (_mainViewModel.CurrentEntity.CurrentEntityType == typeof(UserEntity))
                    _mainViewModel.CurrentEntity = logEntity;
                else
                    _mainViewModel.CurrentEntity = userEntity;
                /*
                 * CurrentEntity = new ListViewModel<LogEntity>();

                 型 'GenericTypeConstraintsPatterns.Entity.LogEntity' はジェネリック型またはメソッド 'ListViewModel<TEntity>' 内で型パラメーター 'TEntity' として使用できません。'GenericTypeConstraintsPatterns.Entity.LogEntity' から 'GenericTypeConstraintsPatterns.Interface.IListItem' への暗黙的な参照変換がありません。 

                 */

            });

            ///System.InvalidOperationException: 'ItemsSource を使用する前に、Items コレクションが空である必要があります。 
            ///対応 ShowInMemory(); → InitilizeComponent()の順
        }

    ///Genericsメソッドにする事で型パラメータを扱う
+        private void ReadCsv<TEntity>()
        {

            if (string.IsNullOrWhiteSpace(_mainViewModel.FilePath))
            {
                MessageBox.Show("CSVファイルが選択されていません。", "エラー", MessageBoxButton.OK, MessageBoxImage.Warning);
                return;
            }

            if (_mainViewModel.CurrentEntity.CurrentEntityType != typeof(UserEntity))
            {
                MessageBox.Show("CSV表示はUserEntityのみ対応しています。", "エラー", MessageBoxButton.OK, MessageBoxImage.Warning);
                return;
            }


            logEntity.Items.Clear();

            ///CsvファイルのLoadの際、ラッパークラスをジェネリック化することで型安全に呼び出す実装 
            ///→ LogEntityでの呼び出しが出来なくなる。
            ///
            var csvloader = new CsvLoader<UserEntity>();
           
            ///null 参照の可能性があるものの逆参照です対応済み
            ///ifブロックで囲った
            if (_mainViewModel.CurrentEntity is ListViewModel<UserEntity> vm)
            {
                // vm は non-null
                vm.Items.Clear();


                foreach (var token in csvloader.CsvLoad(_mainViewModel.FilePath))
                    vm.Items.Add(token);
            }


            Debug.WriteLine(_mainViewModel.CurrentEntity.Items);
            foreach (var item in _mainViewModel.CurrentEntity.Items)
                Debug.WriteLine(item.GetType());
        }

        private void ShowInMemory()
        {
            var data = new[]
            {
            new UserEntity { Id = 1,DisplayName = "Alice", affiliation = "Common People" },
            new UserEntity { Id=2,DisplayName = "Bob",affiliation = "Regee Dancer" },
            new UserEntity { Id=3,DisplayName = "sheepMan", affiliation="Strange UMA" },
            new UserEntity { Id=4,DisplayName = "Molder", affiliation ="FBI Detective" },
            new UserEntity { Id=4,DisplayName = "Scully",affiliation ="FBI SheepLady Detective" }

        };

            //Bindingが壊れないよう、毎回Newするのをやめる
            //別Entityに切り換えるとBindingが破綻する
            //    Current = new ListViewModel<UserEntity>(data);


            if (_mainViewModel.CurrentEntity is ListViewModel<UserEntity> vm)
            {
                vm.Items.Clear();

                foreach (var e in data)
                {
                    vm.Items.Add(e);
                    Debug.WriteLine(vm.GetType());
                }
            }
         }



        /// <summary>
        /// 呼び出し時にILogItemフレームしか使えなくする
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        private void ShowJson<TEntity>() where TEntity : class, ILogItem, new()
        {
            if (string.IsNullOrWhiteSpace(_mainViewModel.FilePath))
            {
                MessageBox.Show("JSONファイルが選択されていません。", "エラー", MessageBoxButton.OK, MessageBoxImage.Warning);
                return;
            }

            
+ // EnTityTyoeの判別
+            if (_mainViewModel.CurrentEntity.CurrentEntityType != typeof(LogEntity))
            {
                MessageBox.Show("Json表示は型情報:JsonEntityのみ対応しています。", "エラー", MessageBoxButton.OK, MessageBoxImage.Warning);
                return;
            }

            //既存Entityのクリア
            userEntity.Items.Clear();

            ///null 参照の可能性があるものの逆参照です対応済み
            ///ifブロックで囲った
            if (_mainViewModel.CurrentEntity is ListViewModel<LogEntity> vm)
            {
                // vm は non-null
                vm.Items.Clear();


            }

         


            try
            {
                /////GenericTypeConstraintsPatterns.Entity.UserEntity' から 'GenericTypeConstraintsPatterns.Interface.IListItem' への暗黙的な参照変換がありません。
                ///JsonRepository にIlogItemがないので追加

                var repository =
                    new JsonRepository<LogEntity>(_mainViewModel.FilePath);

                if (_mainViewModel.CurrentEntity is ListViewModel<LogEntity> logVm)
                {
                    logVm.Items.Clear();
                    foreach (var e in repository.LoadAll())
                        logVm.Items.Add(e);
                }
           

            }
            catch (JsonException ex)
            {
                MessageBoxService.ShowError(ex, ex.Message +"Error reading JSON file");
            }

        }
        

       

        public void FilePatheSelect()
        {
            // ダイアログのインスタンスを生成
            var dialog = new OpenFileDialog() {

                Filter = "CSVファイル (*.csv)|*.csv|JSONファイル (*.json)|*.json|すべてのファイル (*.*)|*.*",
            };
          
            // ファイルの種類を設定
            dialog.DefaultDirectory = "C\\";


            // ダイアログを表示する
            if (dialog.ShowDialog() == true)
            {
                // 選択されたファイル名 (ファイルパス) をメッセージボックスに表示                
                _mainViewModel.FilePath = dialog.FileName;
            }
        }
    }
}

MainViewModel

Current・CurrentEntityとして、現在の型情報を保持します
IListViewModel CurrentEntityViewModelCommand? ViewModelCommands を持ちます。

以下折りたたみ
MainViewModel.cs
using GenericTypeConstraintsPatterns.Entity;
using GenericTypeConstraintsPatterns.Interface;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Input;


namespace GenericTypeConstraintsPatterns.ViewModel
{

    public sealed class MainViewModel : BindableBase
    {
        /// <summary>
        ///    /// <summary>
        /// 型 'TEntity' はジェネリック型またはメソッド 'IReadOnlyRepository<TEntity>' 内で型パラメーター 'TEntity' として使用できません。'TEntity' から 'GenericTypeConstraintsPatterns.Interface.IListDisplayEntity' へのボックス変換または型パラメーター変換がありません。 対応として以下の制約を追加



// IlistViewModel型のプロパティとすることで、間接的に内部プロパティを持たせる
+        public IListViewModel CurrentEntity
        {
            get
            {
                if (field == null)
                {
                    field = new ListViewModel<UserEntity>();

                    Debug.WriteLine(field.GetType());
                }
                    return field;
            }
            set
            {

                SetProperty(ref field, value);
            }
        }





        /// <summary>
        /// CS 9264 Non-nullable プロパティ 'FilePath' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier, or declaring the プロパティ as nullable, or safely handling the case where 'field' is null in the 'get' accessor. 
        /// 対応 NullCheckを追加
        /// </summary>
        public string FilePath
        {
            get
            {

                if (field == null)
                    throw new InvalidOperationException("field is null");

                return field;
            }
            set => SetProperty(ref field, value);
        }


+        public ICommand ToggleEntityCommand { get; }

     
+        public  ViewModelCommand? ViewModelCommands { get; }



        public MainViewModel()
        {
            if(ViewModelCommands is null)
                  ViewModelCommands = new ViewModelCommand(this);


            ToggleEntityCommand = new DelegateCommand(() =>
            {
                if (CurrentEntity is ListViewModel<UserEntity>)
                {
                    CurrentEntity = new ListViewModel<UserEntity>();
                }
                else
                {
                    CurrentEntity = new ListViewModel<LogEntity>();


                    /*
                     * CurrentEntity = new ListViewModel<LogEntity>();

                     型 'GenericTypeConstraintsPatterns.Entity.LogEntity' はジェネリック型またはメソッド 'ListViewModel<TEntity>' 内で型パラメーター 'TEntity' として使用できません。'GenericTypeConstraintsPatterns.Entity.LogEntity' から 'GenericTypeConstraintsPatterns.Interface.IListItem' への暗黙的な参照変換がありません。 
                     
                     */
                }
            });
        }
    }
}


開発に苦労した点

エラー対処 CS 9264 Non-nullable プロパティ 'FilePath' の対処

→ 単にNullcheckすれよかった

暗黙的な参照変換はありません。

Microsoft Learnから
CS0311: 型 'type1' は、ジェネリック型またはメソッド '' の型パラメーター 'T' として使用できません。'type1' から 'type2' への暗黙的な参照変換はありません。
対処 → これは同一のWhere制約にするか、制約を外して最小限のものにすれば大体は対処できます。

WPFでデータが表示されない

1.以下のテンプレートが <Window.Resources>にない


    <Window.Resources>

        <!-- UserEntity -->
        <DataTemplate x:Key="UserEntityTemplate">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Id}" Margin="5"/>
                <TextBlock Text="{Binding DisplayName}" Margin="5"/>
                <TextBlock Text="{Binding affiliation}" Margin="5"/>
            </StackPanel>
        </DataTemplate>

        <!-- LogEntity -->
        <DataTemplate x:Key="LogEntityTemplate">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Timestamp}" Margin="5"/>
                <TextBlock Text="{Binding Level}" Margin="5"/>
                <TextBlock Text="{Binding Message}" Margin="5"/>
            </StackPanel>
        </DataTemplate>

新しくNewしているのも原因でした。コンストラクタで一度だけnewしておけば壊れません。

        ListViewModel<LogEntity> logEntity;
        ListViewModel<UserEntity> userEntity;

        public ViewModelCommand(MainViewModel mainViewModel)
        {
            _mainViewModel = mainViewModel;

            _mainViewModel.FilePath = string.Empty;
            ShowInMemoryCommand = new DelegateCommand(ShowInMemory);
            ShowCsvCommand = new DelegateCommand(ReadCsv<UserEntity>);
            ShowJsonCommand = new DelegateCommand(ShowJson<LogEntity>);
            FilePickerCommand = new DelegateCommand(FilePatheSelect);

             logEntity = new();
             userEntity = new();
        }

自作アプリだとよくやってるんだけどねぇ。

まとめ Where : T(Generic型制約設計)であることの利点・必然性

  • where制約であり得ない組み合わせをコンパイル時に消す
  • Repository 側で where 制約をかける
    • 構造と制約は ViewModel に集約
    • where 制約は 表示可能な組み合わせだけを生成させる

つまり、以下はコンパイルエラーとなる

コンパイルエラー例
 var repo = new CsvRepository
 <UserEntity, UserEntitySupport>(); //正しい

var repo = new CsvRepository
<LogEntity, LogEntitySupport>();
//Where:UserEntity 制約によりエラー!

おまけ 🤡 型を拡張することにより、型システム範囲外の「ファイルを読み込んだ状態」を持たせる方法

「Csvファイルが読み込まれた情報」を型として実装することにより、Where:Tにより明示的実装保証がなされるようにします。

  • 動画
実装

:::note
いいね数が6以上 て実装Codeを執筆します。


FAQ

Q. 実務ではここまで作り込む必要はないのでは?

  • A. 単発の処理であれば確かに不要です。しかし、型で状態を表現しておくことで、class SampleClass<TFileParsed> where TFileParsed : IFileParsedのように 「まだ読み込まれていない CSV を誤って処理する」コードがコンパイル時点で書けなくなります
     
     → 保守視点 : 型で状態を表現しておくことで、読み込み忘れによる実行時エラーが設計段階で排除されます。
    Class<T> :Where 部分の記述を読むことで、明確に実装意図が分かるようになります。背景はCode内コメントで。

利点

  • ifブロック判定が必要なくなる。
    私としては教育用途でMessaageBoxを入れたいのですが、実務レベルでは「このメソッドは必ず CSV 読み込み後に呼ばれる」という前提を、
    コメントや呼び出し順ではなく型で強制できるようになります。
     → 拡張子チェックやNullチェックなど、細かな判定処理も不要になる
    → これは 「パース済みかどうか」を表す bool フラグ自体が不要になる、という意味です。状態は値ではなく型で保証されます。

🐑🐑🐑🐑

  • 呼び出し先でもif判定が不要になり、型レベルでファイルが読み込まれたかどうかが保証される
     → これに SanpleClass : TFi;eParsed Where TFi;eParsed
    などとすれば**、明示的に型そのものにファイル読み込み機能があることが保証されます。** 

🐑🐑🐑🐑🐑🐑🐑🐑

これは将来的にも大きなreturnが見込める技術的投資です。
この設計は 状態不整合の防止呼び出し側の条件分岐削減将来的な機能追加時の誤用防止 につながり、長期的な保守コストを確実に下げます。


実装例2:入力検証的な実装👨‍🎓

※ストック3以上で執筆します

実装例3:コマンド/処理系の実装🪖

※絵文字は多分兵士のヘルメットです。
※ストック6で執筆します。

ではやってくだちい。

あとがき 

あけおめです🗾image.png

今回もやたらと時間が掛かっています。正月休みほとんど潰しました🎍
あと2項目残ってますががんばります。

Hunter×Hunterの女医さん(Twitter投稿から抜粋)
image.png

この記事が冗長で面白くない場合はBadボタン、フォロー解除をお願いします。

この記事に関する免責事項

私はIT業界未経験者です。

可能な限り初心者でも分かりやすい記事を心がけていますが、書いている内容が理解できない場合、コメント欄にてあやまります。<(_ _)>

この記事はストックの進捗状況により、未完成で終わる場合があります。皆さん、がんばってストックしてほしいです。 敬具

14
8
3

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
14
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?