Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

Where:T (ジェネリック型制約)の用途について

Q&A

解決したいこと

生成AIが言うには、暗黙知として以下の3つがあるとされています。

1.データ表示系
class ListViewModel<TEntity> where TEntity : IEntity, INotifyPropertyChanged

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

2 . 入力/検証系
ここは特に無意識レベルです。

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

3 .コマンド/処理系
ここも経験者ほど嫌悪感が強い領域です。

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

これを何度も踏んだ結果、GenericController<TParam>に行き着く。

ChatGPTなどはこう言うのですが、ベテランの方はこれらについてどう考えているのか知りたいです。書くのがめんどうくさい?

検索しても単発型のCoding例しか出ませんので

追記
御回答に感謝しております。
オープンにしておきますので
他にも一家言ある方はお気軽にコメントください🙇‍♀️

2 likes

4Answer

用途となるとケースバイケースなので途端に実例を挙げるのが難しくなりますが。構文の面で見れば、例えばallows ref structインターフェイスを実装した構造体をスタックで扱えるようにする特性を利用するものなので、インターフェイスへのアップキャストによる不要なボックス化を避ける意図が考えられるでしょう

制約というのは、そのジェネリクスが仮想的にどのような親クラス、あるいはインターフェイスを持つかを明示するドキュメントです
インターフェイスに一度アップキャストされたインスタンスの情報は隠蔽されます
ダウンキャストが危険な操作とされるように、抽象化されたデータの具体性(派生クラスの種類)は問わないことが原則です

しかし抽象的なまま扱っては不便な局面もあります
ジェネリクスは抽象的な文脈をある特定のメソッドに限定できるので、多態性(抽象と具象を柔軟に切り替える)と相性が良い利点があります

2Like

Comments

  1. @EndOfData

    Questioner

    allow ref structは最近知りましたが、そういう特性があるのですね。
    ちょっと理解が難しいですが、確かに抽象型のまま扱うのはかなり不便です。自分でこうなのだから利用者はもっと不便だろうとは思いました
    where句で自己参照型にするのも、抽象型を自分のクラスに限定するためですね。

  2. これは個人的な型の捉え方ですが、詰まるところ型は状態(概念の名前)だと思うので、その型で何を表したいかが重要だと思います
    例えば自然数なら1あるいは0以下の状態を取れない代わりに整数->有理数->実数と多相に振る舞えますし、整数は自然数になれない代わりに0以下の数を扱えます
    この関係を仮に継承で表すとします

    public class Integer(int x){
        protected int _number=x;
    }
    
    public class Nat(int x):Integer(x>0?x:1);
    

    この時整数を自然数に具象化することを考えなければ、内部で負の値を扱おうが、それは自然数ではないので安全です
    しかし自然数から整数に抽象化することを考える場合、整数の文脈で自然数を解釈されては困ります
    仮に足し算を実装する場合を考えます

    public class Integer(int x){
        protected int _number=x;
    	public void Add(int x)=>_number+=x>0?x:0;
        public int Number=>_number;
    }
    
    public class Nat(int x):Integer(x>0?x:1);
    

    x.Add(y.Number)のような形でxyを加算する時、これを宣言するメソッドは次のような定義を持つでしょう

    static Nat Plus(Nat x,Nat y){
        x.Add(y.Number);
        return x;
    }
    
    static Integer Plus(Integer x,Integer y){
        x.Add(y.Number);
        return x;
    }
    

    いずれも元の型のオブジェクトのままでは扱えません
    特に後者はダウンキャストを避けるためにIntegerの戻り値を受け取ることが強制されるので、Nat自体を扱うことができなくなります
    ジェネリクスはこのような問題を解消するのに役立ちます
    例えば以下のように実装します

    public static class Cal{
    	public static T Plus<T,Q>(this T x,Q y) where T:Integer where Q:Integer{
    		x.Add(y.Number);
    		return x;
    	}
    }
    

    ジェネリクスで二つの引数の型が異なることを示し、更にその型がどの型から派生するかを指定します
    すると戻り値Tが第一引数で推論された型に固定されるので、第二引数によって推論結果が左右されることを防げます
    Natstring型の固有プロパティを定義してみましょう

    public class Nat(int x):Integer(x>0?x:1){
        public string Message=>"Success!";
    }
    

    Plus<T>を用いることで次のように呼び出せます

    using System;
    					
    public class Program
    {
    	public static void Main()
    	{
    		Integer x=new(-10);
    		Nat y=new(10);
    		Console.WriteLine($"{x.Plus(y).Number}:{x}\n{y.Plus(x).Message}:{y}");
    	}
    }
    
    0:Integer
    Success!:Nat
    

    このように引数と戻り値の型の一意性を保つことができます
    このプログラムでは戻り値を受け取らずともyがある限りNat型のオブジェクトを参照し続けられるのであまり恩恵を感じられないかもしれません
    ただジェネリクスがメソッドにどのような柔軟性をもたらすかは垣間見ることができます

  3. @EndOfData

    Questioner

    引数と戻り値の型との一意性を保つですか。
    計算という文脈だとそのような見方もあるんですね。参考になります。

自分の場合、主に下記の用途で使用します。

・特定の機能を持っている事を保証したい(ジェネリック型のメソッドを呼ぶ、インスタンスを作成する等)
機能的に必要、という至極単純な理由です。

・ジェネリック型に想定外の型が指定された場合、コンパイル時点で弾きたい
普通に使っていれば有り得ない事ではありますが、型レベルで保証されていると余計なチェックやテストをする必要が無くなる場合があります。

2Like

Comments

  1. @EndOfData

    Questioner

    つまり、「その機能を持っている事を明示するためにとりあえず使う」みたいな感じですね。

    qiita内でも探すとwhere:T制約は結構見つかるのですが、やはりAIより生の声が参考になります。ありがとうございます

@EndOfData さん

自分は設計を主導する立場というより、他の方が設計したコードを読む・使う側として
ジェネリック型制約に触れてきた経験が多いです。

その視点で見ると、where 句が書かれているコードは
「この型は何のために使うものか」「どんな前提を持つデータか」が
コードを追わなくても分かることが多く、読み手として助けられてきました。

特に、表示用ViewModel・入力検証・コマンド処理といった場面では、
object や実行時キャストに頼らず、型で前提条件を明示している設計のほうが
後から触ったときに安全で、if 文や null チェックも少なく済む印象があります。

「書くのが面倒」というより、読む側・使う側が前提を誤解したり、
想定外の使い方や実装になりにくくするための工夫 (おそらく? or 結果としてそうなってる?) として、使われているケースを多く見てきました。

一方で、制約を増やしすぎると汎用性が下がったり、
将来の変更時に影響範囲が大きくなる設計も見てきたため、
どこまで型で縛るかはチームや文脈次第だとも感じています。

1Like

Comments

  1. @EndOfData

    Questioner

    そうですね。障害が出たらその都度、型制約を緩くしていかないと設計が破綻します。
    これは実際に作り込んでみて感じた部分です。

    なるほど、読み手の理解を助ける側面が強いのですね。参考になります。

このお題はQ&Aより意見交換がふさわしいような…

これから学ぶ人にとっては「ジェネリック型制約」より「ジェネリック型」自体の方がずっとわかりにくく、何のためにあるのか何の役に立つのが疑問を持つんじゃないかと思います。

そしてそこをクリアして自分でジェネリックなクラスやメソッドを実装しようとすると、型制約がないと不便な状況はわりとすぐに発生します。
必要ないのはList<T>のような「型は指定するがそのオブジェクトに対する操作は一切行わないもの」に限られるでしょう。こういうタイプも少なくないからこそ、このお題が出てきたとも言えそうですが。

1Like

Your answer might help someone💌