LoginSignup
4
3

More than 3 years have passed since last update.

【C#】Genericの型を値型に限定したい

Last updated at Posted at 2019-10-31

はじめに

C#のGenericは非常によく利用されます。
このGenericはwhereによって指定できる型をある程度制限できますが、こと値型のみに限定するということができません。

ここで言う値型とは

  • byte
  • sbyte
  • short
  • ushort
  • int
  • uint
  • long
  • ulong
  • float
  • double
  • decimal

を指します。

ただし、これらの型が共通で継承しているinterfaceをwhereで指定することでそこそこに限定できるようです。

確認環境

Unity 2019.2.0f1
IL2CPP
Android

実装

実装としてはこれだけです。
例はメソッドですがクラスでも一緒です。

void Hoge<T>() where T : struct, IComparable, IFormattable, IConvertible, IComparable<T>, IEquatable<T>
{
}

これらのinterfaceを全て実装している独自のstructなどがあればGenericに指定できてしまいますが、ほぼほぼ無いと思うのでこれで大丈夫でしょう。
実際に最初に記述した値型以外の型を指定するとコンパイルエラーになります。

参考

注意

全ての型のリファレンスを確認したわけではないのでもしかしたらこれらの型以外にも一致するものがあるかもしれません。

おまけ

この制約を利用して指定した型で合計と平均を集計するクラスを実装してみました。

IL2CPPでの実装

計算を行う必要がありますが、Tのままでは計算ができないため、計算時は全てdecimalに変換しています。

AggregateIL2CPP.cs
namespace MyEngine
{
    using System;

    /// <summary>
    /// データ集計管理クラス
    /// </summary>
    public class Aggregate<T> where T : struct, IComparable, IFormattable, IConvertible, IComparable<T>, IEquatable<T>
    {
        #region Properties

        /// <summary> 合計値 </summary>
        public T Sum { get; private set; }

        /// <summary> 平均値 </summary>
        public T Average { get; private set; }

        /// <summary> 集計データのサンプル数 </summary>
        public int SampleNum { get; private set; }

        #endregion


        #region Constructor

        public Aggregate()
        {
            Clear();
        }

        #endregion


        #region Methods

        private T ConvertValue(object data)
        {
            return (T)Convert.ChangeType(data, typeof(T));
        }

        #endregion


        #region API

        /// <summary>
        /// 集計データを初期化する
        /// </summary>
        public void Clear()
        {
            SampleNum = 0;
            Sum = ConvertValue(0);
            Average = ConvertValue(0);
        }

        /// <summary>
        /// 集計データを追加
        /// </summary>
        /// <param name="data"></param>
        public void Add(T data)
        {
            SampleNum++;
            Sum = ConvertValue(Sum.ToDecimal(null) + data.ToDecimal(null));
            Average = ConvertValue(Sum.ToDecimal(null) / SampleNum);
        }

        #endregion
    }
}

Monoでの実装

Monoの場合はConvertをdynamicに置き換えることができます。

AggregateMono.cs
namespace MyEngine
{
    using System;

    /// <summary>
    /// データ集計管理クラス
    /// </summary>
    public class Aggregate<T> where T : struct, IComparable, IFormattable, IConvertible, IComparable<T>, IEquatable<T>
    {
        #region Properties

        /// <summary> 合計値 </summary>
        public T Sum { get; private set; }

        /// <summary> 平均値 </summary>
        public T Average { get; private set; }

        /// <summary> 集計データのサンプル数 </summary>
        public int SampleNum { get; private set; }

        #endregion


        #region Constructor

        public Aggregate()
        {
            Clear();
        }

        #endregion


        #region API

        /// <summary>
        /// 集計データを初期化する
        /// </summary>
        public void Clear()
        {
            SampleNum = 0;
            Sum = (dynamic)0;
            Average = (dynamic)0;
        }

        /// <summary>
        /// 集計データを追加
        /// </summary>
        /// <param name="data"></param>
        public void Add(T data)
        {
            SampleNum++;
            Sum += (dynamic)data;
            Average = (dynamic)Sum / SampleNum;
        }

        #endregion

    }
}

使用方法

適当なオブジェクトにアタッチしたら実行できます。
Aggregateのintを他の型に変えるとその型で集計するようになります。
試しにfloatにすると少数まで集計します。

PCの場合
 左クリック : 一回のみ集計
 右クリック : 毎フレーム集計

Android実機の場合
 1本指でタッチ : 一回集計
 2本指でタッチ : 毎フレーム集計
 3本指でタッチ : 集計クリア

AggregateTest.cs
using UnityEngine;
using MyEngine;

using Random = System.Random;

public class AggregateTest : MonoBehaviour
{
    Aggregate<int> _aggregate = new Aggregate<int>();

    // Start is called before the first frame update
    void Start()
    {
    }

    // Update is called once per frame
    void Update()
    {
        bool isUpdate = false;

#if UNITY_EDITOR
        if (Input.GetMouseButtonDown(0) || Input.GetMouseButton(1))
        {
            isUpdate = true;
        }
#else

        if (Input.touchCount == 1 && Input.touches[0].phase == TouchPhase.Began)
        {
            isUpdate = true;
        }
        else if (Input.touchCount == 2)
        {
            isUpdate = true;
        }
        else if (Input.touchCount == 3)
        {
            _aggregate.Clear();
        }
#endif

        if (isUpdate)
        {
            Random rand = new Random();
            _aggregate.Add((int)(rand.NextDouble() * 100));
        }
    }

    private void OnGUI()
    {
        GUILayout.Label($"合計={_aggregate.Sum:N}\n平均={_aggregate.Average:N}\nサンプル数={_aggregate.SampleNum:N}");
    }
}

4
3
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
4
3