Home

Awesome

GenericEnhance

日本語

Summary

This library "GenericEnhance" provides strong support for Generic programming.

These incorporate some of the features that were available in C++ templates for use in C# Generic programming.

System Requirements

EnvironmentVersion
Unity2021.3.38f1, 2022.3.20f1
.Net4.x, Standard 2.1

How to install

Install dependenies

Install the following packages.

Installing GenericEnhance

  1. Open [Window > Package Manager].
  2. click [+ > Add package from git url...].
  3. Type https://github.com/Katsuya100/GenericEnhance.git?path=packages and click [Add].

If it doesn't work

The above method may not work well in environments where git is not installed.
Download the appropriate version of com.katuusagi.genericenhance.tgz from Releases, and then [Package Manager > + > Add package from tarball...] Use [Package Manager > + > Add package from tarball...] to install the package.

If it still doesn't work

Download the appropriate version of Katuusagi.GenericEnhance.unitypackage from Releases and Import it into your project from [Assets > Import Package > Custom Package].

Generic Specialization

*This feature is limited to Generic methods only.

Generic specialization is a mechanism that calls different implementations depending on the type arguments given.
This allows you to

This makes it possible to do things such as

How to implementation

Specialization is done using the SpecializationMethod and SpecializedMethod attributes.
The SpecializationMethod attribute specifies the function to be called by default.
The SpecializedMethod attribute specifies the function to be called at the time of specialization and the type argument of the specialization condition.
In the example, GetValue method is specialized.

using Katuusagi.GenericEnhance;

[SpecializationMethod(nameof(GetInternal))]  // Basically, GetInternal is called.
[SpecializedMethod(nameof(GetInteger), typeof(int))]  // GetInteger is called if the type argument contains an int.
public partial T GetValue<T>();

public T GetInternal<T>()
{
    return default;
}

public int GetInteger()
{
    return 100;
}

:

// GetValueInternal is called.
var f = GetValue<float>();
Debug.Log(f); // 0

// GetInteger is called.
var i = GetValue<int>();
Debug.Log(i); // 100;

Algorithm specification

There are several algorithms for specialization.
The algorithm can be specified by the second argument of the SpecializationMethod attribute.
Since each algorithm has different characteristics, you should use the algorithm according to the actual implementation environment.

Algorithmfeature
VirtualStrategyIt is implemented in a strategy pattern using virtual method.<br>Creates a cache on the first call.<br>The overhead of the virtual function call is incurred.
DelegateStrategyIt is implemented in a strategy pattern using Delegate.<br>Creates a cache on the first call.<br>After that, a small cache is created for each type combination.<br>Calls are fast.
TypeComparisonLinear comparison of types.<br>No cache is created.<br>It is very fast in the JIT environment because it is optimized.
TypeIdComparisonComparisons are made using IDs assigned to type combinations.<br>A cache is created for type comparisons.<br>After that, a small cache is created for each type combination.<br>It can be called at high speed even in an AOT environment.

See below for results of performance measurements.

Measurements on the Editor

Test Code

ProcessPerformance
Direct0.014535 ms
VirtualStrategy0.07683 ms
DelegateStrategy0.067435 ms
TypeComparison0.036765 ms
TypeIdComparison0.04558 ms

The TypeComparison gives high performance because the editor allows JIT compilation.

Post-build measurements

The measurement code is as follows.

private readonly ref struct Measure
{
    private readonly string _label;
    private readonly StringBuilder _builder;
    private readonly float _time;

    public Measure(string label, StringBuilder builder)
    {
        _label = label;
        _builder = builder;
        _time = (Time.realtimeSinceStartup * 1000);
    }

    public void Dispose()
    {
        _builder.AppendLine($"{_label}: {(Time.realtimeSinceStartup * 1000) - _time} ms");
    }
}

:

var log = new StringBuilder();
int testCount = 10000;
using (new Measure("Direct", log))
{
    for (int i = 0; i < testCount; ++i)
    {
        Add(10.0, 20.0);
    }
}

using (new Measure("Static Strategy", log))
{
    for (int i = 0; i < testCount; ++i)
    {
        WrappedAdd_VirtualStrategy<double, double, double>(10, 20);
    }
}

:
ProcessMono(Debug)Mono(Release)IL2CPP(Debug)IL2CPP(Release)
Direct0.01612091 ms0.002952576 ms0.0004882813 ms0 ms(unmeasurable)
VirtualStrategy0.161232 ms0.06460953 ms2.692383 ms2.297852 ms
DelegateStrategy0.1258011 ms0.06257629 ms0.05908203 ms0.04199219 ms
TypeComparison0.08607483 ms0.02434921 ms5.053223 ms3.029297 ms
TypeIdComparison0.0706749 ms0.03424454 ms0.07226563 ms0.07714844 ms

Both are inferior to direct calls, but if judged solely on processing speed, the following are recommended.

If you do not consider differences due to environment, TypeIdComparison will give you stable performance.

However, since the timing and frequency of cache creation varies depending on the algorithm, please select one in consideration of the intended use.
VirtualStrategy has a smaller cache size and less frequent cache creation, so it has a lower negative impact on performance.
TypeComparison does not create a cache and therefore offers the highest performance in terms of memory.

Replacement Optimization

Only if the following conditions are met will the call be replaced by a direct call, regardless of the algorithm.
The same performance as a direct call can be achieved, so active use of this feature is recommended.

  1. The specified function is public.
[SpecializationMethod(nameof(GetInternal))]
[SpecializedMethod(nameof(GetInteger), typeof(int))]
public partial T GetValue<T>();

// OK
public T GetInternal<T>()
{
    return default;
}

// NG
private T GetInternal<T>()
{
    return default;
}

:
  1. Do not give other type arguments to type arguments.
// OK
public int WrappedGetValue<int>()
{
    return GetValue<int>();
    // ↓
    // return GetInteger();
}

// NG
public T WrappedGetValue<T>()
{
    return GetValue<T>();
}

TypeDef

The TypeDef attribute can be used to redefine an already existing type with an alias.
Besides its use as an alias, it can also be used to return a type generated from within Generic.

How to Implementation

Redefine the type by implementing the following.

using Katuusagi.GenericEnhance;

[TypeDef(typeof(int))]
public struct DefInt
{
}

Redefining Members

A type with the TypeDef attribute can have a member of the same name to call the member that the original type has.

using Katuusagi.GenericEnhance;

[TypeDef(typeof(Vector3))]
public struct Vector
{
    public float x;
    public float magnitude { get; }
}

:

Vector v = default;
v.x = 10.0f
Debug.Log(v.magnitude); // 10

Redefinition of Type Arguments

If you want to redefine a Type Argument, specify the type name as a string.
You can also use the nameof clause.

using Katuusagi.GenericEnhance;

public struct Any<T>
    where T : struct
{
    [TypeDef(nameof(T))]
    public struct Element
    {
    }
}

:

Debug.Log(typeof(Any<int>.Element)); // Int32

Reinterpretation of type

Types with the TypeDef attribute are replaced at the IL level.
However, from the C# side, they are interpreted as different types.
If you want to reinterpret the type from C#, use the CastUtils.SafeAs function.

[TypeDef(typeof(int))]
public struct DefInt
{
}

:

int intValue = 100;
DefInt defaultValue = default;
ref DefInt defIntValue = ref CastUtils.SafeAs(ref intValue, ref defaultValue);

Debug.Log(defIntValue); // 100

Use the CastUtils.TryAs function to detect reinterpretation failures.

int intValue = 100;
if (!CastUtils.TryAs(ref intValue, out DefInt defIntValue))
{
    // failed.
}

Debug.Log(defIntValue); // 100

Note

Value types may specify the TypeDef attribute only between value types, and reference types may specify it only between reference types.

using Katuusagi.GenericEnhance;

// OK
[TypeDef(typeof(int))]
public struct DefInt
{
}

// NG
[TypeDef(typeof(int))]
public class DefInt
{
}

Generic Non-type argument

After the introduction of this library, it is possible to give untyped arguments as Generic arguments.
Strictly speaking, nontype information is wrapped in type information and passed as a generic argument.
This allows non-type information to be passed from type arguments and guarantees constant nature.
When used in conjunction with Generic specialization, it is also possible to branch using type information.

How to Implementation

The notation for non-type parameters is as follows

TypeExampleNotation
Positive Integer100_100
Negative Integer-100_n100
Positive Decimal Fraction10.1_10_1
Negative Decimal Fraction-10.1_n10_1
Truetrue_true
Falsefalse_false

The runtime value is obtained with the TypeFormula.GetValue method.

var value = TypeFormula.GetValue<_100, int>();
Debug.Log(value); // 100

Operation

To perform operations, use the Operator type.
The following example uses the Add type, which is an Operator type for addition.

var value = TypeFormula.GetValue<Add<int, _100, _10>, int>();
Debug.Log(value); // 110

Type of Operator

Arithmetic Operations
OperatorImplementation
Add<T, TX, TY>(T)TX + (T)TY
Sub<T, TX, TY>(T)TX - (T)TY
Mul<T, TX, TY>(T)TX * (T)TY
Div<T, TX, TY>(T)TX / (T)TY
Mod<T, TX, TY>(T)TX % (T)TY
Minus<T, TX>-((T)TX)
Conditional Logical Operations
OperatorImplementation
Equal<T, TX, TY>(T)TX == (T)TY
NotEqual<T, TX, TY>(T)TX != (T)TY
Greater<T, TX, TY>(T)TX > (T)TY
GreaterOeEqual<T, TX, TY>(T)TX >= (T)TY
Less<T, TX, TY>(T)TX < (T)TY
LessOeEqual<T, TX, TY>(T)TX <= (T)TY
And<TX, TY>(bool)TX && (bool)TY
Or<TX, TY>(bool)TX || (bool)TY
Not<TX>!(bool)TX
Bit Logical Operations
OperatorImplementation
BitAnd<T, TX, TY>(T)TX & (T)TY
BitOr<T, TX, TY>(T)TX | (T)TY
BitXor<T, TX, TY>(T)TX ^ (T)TY
BitNot<T, TX>~(T)TX
LShift<T, TX, TY>(T)TX << (int)TY
RShift<T, TX, TY>(T)TX >> (int)TY
Cast Operations
OperatorImplementation
CastNumeric<T, TX>(Numeric Type)(T)TX

Use TypeDef to treat types like constants

Operator types are subject to replacement by TypeDef.
So the types defined by TypeDef can be treated like constants.

[TypeDef(typeof(Add<int, _100, _200>))]
public struct IntValue : ITypeFormula<int>
{
    int ITypeFormula<int>.Result => default;
}

:

var value = TypeFormula.GetValue<Add<int, IntValue, _10>, int>();
Debug.Log(value); // 310

Replacement Optimization

If a different type argument is not given, the result of the operation is replaced.

// OK
Debug.Log(typeof(Add<int, _100, _10>)); // _110
// ↓
// Debug.Log(typeof(_110));

// NG
Debug.Log(typeof(Add<int, _100, T>)); // Add<int, _100, T>

None Type

A NoneType argument of type NoneType disables that argument internally.
This is mainly used as padding for Generic arguments.

How to Implementation

If NoneType type is specified as a type argument, the type argument is omitted.
Also, if you specify NoneType.Default as a type argument, that argument will be omitted.

Debug.Log(typeof(Action<int, NoneType>)); // Action`1
Debug.Log(typeof(Action<NoneType>)); // Action

Action<int, NoneType> action = (x, y) => {};
action(0, NoneType.Default);
// ↓
// action(0);

Variadic type arguments

*This function is limited to the Generic method.

Duplicates the type argument up to the specified count.
Note that this is not a variadic type arguments in the strict sense.

How to Implementation

VariadicGeneric attribute is used to implement variadic type arguments.
The VariadicGeneric attribute specifies the minimum count of elements in the type argument and the maximum count of elements in the type argument.

[VariadicGeneric(0, 16)]
public static Type GetActionType<T>()
{
    return typeof(Action<T>);
}

The implementation is then replicated as follows.

[global::Katuusagi.GenericEnhance.VariadicGenerated(0)]
[VariadicGeneric(0, 16)]
public static Type GetActionType()
{
    return typeof(Action<global::Katuusagi.GenericEnhance.NoneType>);
}

:

[global::Katuusagi.GenericEnhance.VariadicGenerated(16)]
[VariadicGeneric(0, 16)]
public static Type GetActionType<T__0, T__1, T__2, T__3, T__4, T__5, T__6, T__7, T__8, T__9, T__10, T__11, T__12, T__13, T__14, T__15>()
{
    return typeof(Action<T__0, T__1, T__2, T__3, T__4, T__5, T__6, T__7, T__8, T__9, T__10, T__11, T__12, T__13, T__14, T__15>);
}

Only the trailing type argument is duplicated.

Variadic arguments

This implementation can duplicate the number of elements in the argument by the same number of elements as the Variadic type arguments.
If you want to duplicate an argument, the argument must end with the same type as the trailing type argument.
An example is shown below.

[VariadicGeneric(0, 16)]
public static void InvokeAction<TVar>(Action<TVar> action, TVar args)
{
    action?.Invoke(args);
}

In this case, the implementation is replicated as follows.

[global::Katuusagi.GenericEnhance.VariadicGenerated(0)]
[VariadicGeneric(0, 16)]
public static void InvokeAction(Action<global::Katuusagi.GenericEnhance.NoneType> action)
{
    action?.Invoke(global::Katuusagi.GenericEnhance.NoneType.Default);
}

:

[global::Katuusagi.GenericEnhance.VariadicGenerated(16)]
[VariadicGeneric(0, 16)]
public static void InvokeAction<TVar__0, TVar__1, TVar__2, TVar__3, TVar__4, TVar__5, TVar__6, TVar__7, TVar__8, TVar__9, TVar__10, TVar__11, TVar__12, TVar__13, TVar__14, TVar__15>(Action<TVar__0, TVar__1, TVar__2, TVar__3, TVar__4, TVar__5, TVar__6, TVar__7, TVar__8, TVar__9, TVar__10, TVar__11, TVar__12, TVar__13, TVar__14, TVar__15> action, TVar__0 args__0, TVar__1 args__1, TVar__2 args__2, TVar__3 args__3, TVar__4 args__4, TVar__5 args__5, TVar__6 args__6, TVar__7 args__7, TVar__8 args__8, TVar__9 args__9, TVar__10 args__10, TVar__11 args__11, TVar__12 args__12, TVar__13 args__13, TVar__14 args__14, TVar__15 args__15)
{
    action?.Invoke(args__0, args__1, args__2, args__3, args__4, args__5, args__6, args__7, args__8, args__9, args__10, args__11, args__12, args__13, args__14, args__15);
}

Extracting type argument element in an iterative process

By writing VariadicForEach scope, you can iterate over variable length type arguments like a foreach statement.
Within the VariadicForEach scope, type arguments and arguments are not expanded when invoked.
All elements are called repeatedly.
The following example uses the VariadicForEach scope to concatenate variable-length arguments.

[VariadicGeneric(1, 16)]
public static string ForEachJoin<T>(string separator, T args)
{
    var str = string.Empty;
    using (new VariadicForEach())
    {
        str = $"{str}{(T)args}{separator}";
    }

    if (string.IsNullOrEmpty(str))
    {
        return string.Empty;
    }

    return str.Remove(str.Length - separator.Length, separator.Length);
}

In this case, the iterative process is generated as follows.


[global::Katuusagi.GenericEnhance.VariadicGenerated(16)]
[VariadicGeneric(1, 16)]
public static string ForEachJoin<T__0, T__1, T__2, T__3, T__4, T__5, T__6, T__7, T__8, T__9, T__10, T__11, T__12, T__13, T__14, T__15>(string separator, T__0 args__0, T__1 args__1, T__2 args__2, T__3 args__3, T__4 args__4, T__5 args__5, T__6 args__6, T__7 args__7, T__8 args__8, T__9 args__9, T__10 args__10, T__11 args__11, T__12 args__12, T__13 args__13, T__14 args__14, T__15 args__15)
{
    var str = string.Empty;
    using (new VariadicForEach())
    {
        {
            str = $"{str}{(T__0)args__0}{separator}";
        }
        global::Katuusagi.GenericEnhance.VariadicUtils.ContinueTarget();

        :

        {
            str = $"{str}{(T__15)args__15}{separator}";
        }
        global::Katuusagi.GenericEnhance.VariadicUtils.ContinueTarget();
    }
    if (string.IsNullOrEmpty(str))
    {
        return string.Empty;
    }
    return str.Remove(str.Length - separator.Length, separator.Length);
}

continue clause and break clause

Variadic.Break and VariadicUtils.Continue methods can be used to express break and continue clauses.

using (new VariadicForEach())
{
    if (isBreak)
    {
        Variadic.Break();
    }

    if (isContinue)
    {
        Variadic.Continue();
    }
}

Expand and Pick

Use the ExpandVariadicParameters scope if you want to expand type arguments or arguments inside the VariadicForEach scope.

using (new VariadicForEach())
{
    using (new ExpandVariadicParameters()
    {
        Action<T> action = ...;
        action?.Invoke(args);
        // ↓
        // Action<T__0, T__1, ...> action = ...;
        // action?.Invoke(args__0, args__1, ...);
    }
}

In addition, the PickVariadicParameter scope can again be used to retrieve individual type argument elements.

using (new VariadicForEach())
{
    using (new ExpandVariadicParameters()
    {
        :

        using(new PickVariadicParameter())
        {
            Debug.Log((T)args);
            // ↓
            // Debug.Log((T__x)args__x);
        }
    }
}

Check the count of elements in the type argument

You can use VariadicUtils.VariadicParameterCount to get the number of elements in a variable-length argument.
The following example converts the given arguments to an array.

[VariadicGeneric(1, 16)]
public static object[] MakeArray<T>(T args)
{
    int i = 0;
    var values = new object[VariadicUtils.VariadicParameterCount];
    using (new VariadicForEach())
    {
        values[i] = args;
        ++i;
    }

    return values;
}

Recursively retrieve type argument element.

There is also a technique of sequential retrieval from the first element by recursive processing.
The following example concatenates variadic arguments.

public static string RecursiveJoin<TFirst>(string separator, TFirst arg)
{
    return arg.ToString();
}

[VariadicGeneric(1, 16)]
public static string RecursiveJoin<TFirst, TVar>(string separator, TFirst arg, TVar argVars)
{
    var str = RecursiveJoin<TVar>(separator, argVars);
    return $"{arg}{separator}{str}";
}

The following code is generated.

[global::Katuusagi.GenericEnhance.VariadicGenerated(2)]
[VariadicGeneric(1, 16)]
public static string RecursiveJoin<TFirst, TVar__0, TVar__1>(string separator, TFirst arg, TVar__0 argVars__0, TVar__1 argVars__1)
{
    var str = RecursiveJoin<TVar__0, TVar__1>(separator, argVars__0, argVars__1);
    return $"{arg}{separator}{str}";
}

:

[global::Katuusagi.GenericEnhance.VariadicGenerated(16)]
[VariadicGeneric(1, 16)]
public static string RecursiveJoin<TFirst, TVar__0, TVar__1, TVar__2, TVar__3, TVar__4, TVar__5, TVar__6, TVar__7, TVar__8, TVar__9, TVar__10, TVar__11, TVar__12, TVar__13, TVar__14, TVar__15>(string separator, TFirst arg, TVar__0 argVars__0, TVar__1 argVars__1, TVar__2 argVars__2, TVar__3 argVars__3, TVar__4 argVars__4, TVar__5 argVars__5, TVar__6 argVars__6, TVar__7 argVars__7, TVar__8 argVars__8, TVar__9 argVars__9, TVar__10 argVars__10, TVar__11 argVars__11, TVar__12 argVars__12, TVar__13 argVars__13, TVar__14 argVars__14, TVar__15 argVars__15)
{
    var str = RecursiveJoin<TVar__0, TVar__1, TVar__2, TVar__3, TVar__4, TVar__5, TVar__6, TVar__7, TVar__8, TVar__9, TVar__10, TVar__11, TVar__12, TVar__13, TVar__14, TVar__15>(separator, argVars__0, argVars__1, argVars__2, argVars__3, argVars__4, argVars__5, argVars__6, argVars__7, argVars__8, argVars__9, argVars__10, argVars__11, argVars__12, argVars__13, argVars__14, argVars__15);
    return $"{arg}{separator}{str}";
}

The elements are taken out in order from the top by decreasing the number of arguments one by one and recursing.

Default type argument

*This function is limited to the Generic method.

Specifying a default type argument for a type argument allows the type argument to be omitted.

How To Implementation

Use the DefaultType attribute to specify a default type argument.
The following example specifies the default type of int for T2 type arguments.
The default type argument must be specified from the end.

public static string ToString<T1, [DefaultType(typeof(int))] T2>(T1 a, T2 b = default)
{
    return $"{a}, {b}";
}

:

var s = ToString(5);
Debug.Log(s); // 5, 0