LoginSignup
6
6

More than 1 year has passed since last update.

[C#]継承してないクラスに無理矢理interfaceを作る

Last updated at Posted at 2021-10-28

やろうと思った経緯

開発中、サーバーとの通信用のクラス(〇〇Responseなど)を自動生成してくれるツールを使っていたのですが、継承などをすることが出来ないため、処理の汎用化をする際に苦労していました。
そこで解決策として「親クラスがなければ親クラスを作れればいいじゃない!」と思ったのが今回の記事のきっかけです。

具体的にどうするの

例を挙げながら説明します。
以下のようなランキング用のResponseクラスがあると仮定し、rankを元に昇順にソートしたいのでrankを抽象化したいと思います。

public class SimpleRankingResponse
{
    public int rank;
    public string userName;
}

このクラスをプロパティーとして持つだけのinterfaceを作成します。これらのinterfaceが全ての基底となります。

public interface IWrapper
{
    object Entity { get; }
}
public interface IWrapper<out T> 
{
    T Entity { get; }
}

次にrankを抽象化するだけのinterfaceを作成します。

public interface IRankingResponse : IWrapper
{
    int Rank { get; }
}

次にそれらを実装するクラスを作成します。

public class RankingResponseWrapper<T> : IWrapper<T>, IRankingResponse
{
    object IWrapper.Entity => Entity;
    public T Entity { get; set; }
    public int Rank { get; set; }
}

次にSimpleRankingResponseIRankingResponseに変換する拡張メソッドを作成します。

public static IRankingResponse ToIRankingResponse(this SimpleRankingResponse rankingResponse)
{
    return new RankingResponseWrapper<SimpleRankingResponse>()
    {
        Entity = rankingResponse,
        Rank = rankingResponse.rank
    };
}

これで準備はできたので使ってみます。

public class Hoge{
    public static void Main(){
        // 流し込むデータ
        var simpleRankingResponseList = new List<SimpleRankingResponse>()
        {
            new SimpleRankingResponse()
            {
                rank = 3,
                userName = "user3"
            },
            new SimpleRankingResponse()
            {
                rank = 2,
                userName = "user2"
            },
            new SimpleRankingResponse()
            {
                rank = 1,
                userName = "user1"
            },
        };

        var rankingView = new RankingView();
        // IRankingResponseに変換してViewに流し込む
        rankingView.ShowRanking(simpleRankingResponseList.Select(x => x.ToIRankingResponse()));
    }
}

public class RankingView
{
    public void ShowRanking(IEnumerable<IRankingResponse> rankingResponseList)
    {
        // IRankingResponseのRankを元に昇順で並べ替え
        foreach (var rankingResponse in rankingResponseList.OrderBy(x => x.Rank))
        {
            // 元のクラスごとにキャストし場合分け
            if (rankingResponse is IWrapper<SimpleRankingResponse> simpleRankingResponse)
            {
                System.Console.WriteLine($"順位:{simpleRankingResponse.Entity.rank}位 名前:{simpleRankingResponse.Entity.userName}");
            }
        }
    }
}

これにより擬似的にinterfaceを追加することが出来ました。クライアント側だけで解決できるので一つの選択肢になるんじゃないかなと思います。
新しいクラスを追加する際は、Wrapperクラスの実装と変換用の拡張メソッドの定義だけです。
後からコメントで気づいたのですが、変換用の拡張メソッドの定義だけで大丈夫です!

気をつける点

元のクラスの値変更

上の例だと元クラスの値が変更される場合は対応できてないので、工夫が必要です。

public class RankingResponseWrapper2<T> : IWrapper<T>, IRankingResponse
{
    object IWrapper.Entity => Entity;
    public T Entity { get; set; }
    public int Rank => rankFunc();
    public Func<int> rankFunc;
}

このようにすれば対応することが出来ますが、クロージャとなるのでGCAllocの値には注意が必要です。
先ほどのRankingResponseWrapperとのGCAllocの比較がこちらです。一個Funcを追加しただけなのに80B->216Bに増えているのが確認できます。
スクリーンショット 2021-10-26 16.06.11.png

IRankingResponse.Entityへのアクセス

実はIRankingResponseIWrapperを継承しているので.Entityという形でもアクセスできます。

if (rankingResponse.Entity is SimpleRankingResponse simpleRankingResponse)
{
    System.Console.WriteLine($"順位:{simpleRankingResponse.rank}位 名前:{simpleRankingResponse.userName}");
}

ただしこの場合System.Object経由になるので、参照型をラップする場合は基本問題ないですが、いくつかの点で注意が必要です。

値型のラップ

値型(主にStruct)をラップすると、System.Object(参照型)に変換しているのでboxingにより32B程のGCAllocが発生しするので注意が必要です。
IWrapper<T>経由でアクセスした場合はZero Allocationです。
スクリーンショット 2021-10-26 16.17.53.png

UnityEngine.Objectのラップ

UnityEngine.Object(主にMonoBehaviour)をラップする際も注意する必要があります。
内部でEquals()などをoverrideしているため、System.Objectのまま扱ってしまうとnull比較などの挙動が変わってしまいます。(キャストしてアクセスすれば問題なし)

応用

Responseなどによくみられる、以下のような入れ子になっているクラスも対応することができます。

public class SimpleRankingResponse
{
    public int rank;
    public string userName;
}
public class SimpleRankingListResponse
{
    // SimpleRankingResponseをリストとして持つ
    public List<SimpleRankingResponse> rankingList;
}
public interface IRankingListResponse : IWrapper
{
    public IEnumerable<IRankingResponse> RankingList { get; }
}
public class RankingListResponseWrapper<T> : IWrapper<T>, IRankingListResponse
{
    object IWrapper.Entity => Entity;
    public T Entity { get; set; }
    public IEnumerable<IRankingResponse> RankingList { get; set; }
}
public static IRankingListResponse ToIRankingListResponse(this SimpleRankingListResponse rankingListResponse)
{
    return new RankingListResponseWrapper<SimpleRankingListResponse>()
    {
        Entity = rankingListResponse,
        RankingList = rankingListResponse.rankingList.Select(x => x.ToIRankingResponse())
    };
}

最後に

今回の発想はGCAllocの調査でIEnumerable関連のソースコードを見ていたのがきっかけでした。
特にList<T>はGCAllocを回避すべく芸術的なことをしているので、一見の価値はあると思います。
他にもいろいろ解決方法(ReflectionやIgnoresAccessChecksToAttributeなど)がありますが、今回の方法が一つの解決策となれば幸いです。

6
6
4

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
6
6