ゲーム化!tomo_manaのブログ

ゲーム化!tomo-manaのブログ

Unityでゲームを作る方法について紹介しています

Unity学習#19-1 (Unity 2019.4.1f1) C#で基底クラスのインタフェースで派生クラスの関数にアクセスしたい場合

第19回で、リストの内容をリストの外(セーブデータなど)から渡す処理を実装するにあたって、継承とインターフェースについて整理しました。他の言語(C++Java)との違いで少し混乱したためです。

やりたいこと

基底クラス インスタンス = new 派生クラス();
インスタンス.関数();

継承を使用した場合

基底public virtual + 派生public override

継承(Extend) を使用する場合、基底クラスへの参照が派生クラスの関数を呼ぶ条件は、

  • 基底クラスの関数が virtual
  • 派生クラスの関数が override

を満たす必要があります。

ideone.comで確認

// 基底クラス
public class super_virtual {
    public virtual void test()
    {
        Console.WriteLine("super_virtual\n");
    }
//    public virtual void test2();  // error CS0501: `super_virtual.test2()' must have a body because it is not marked abstract, extern, or partial
}
// 派生クラス
public class sub_override_super_virtual : super_virtual {
    public override void test()
    {
        Console.WriteLine("sub_override\n");
    }
}
public class Test
{
    public static void Main()
    {
        // 基底クラス インスタンス = new 派生クラス();
        super_virtual sv2 = new sub_override_super_virtual();
        // インスタンス.関数();
        sv2.test();  // sub_override
    }
}

virtual 関数も実装が必要

C#の virtual 関数は普通の関数と同じく実装できます(というより実装が必須です)。定義だけで実装が無いとコンパイルエラーになります(error CS0501)。この点は C++の virtual 関数 や Java の abstract 関数 のイメージがあると少し混乱する点だと思いました。また、C# は virtual 関数を定義するために抽象クラス(abstract)にする必要がありません。クラス内に非 virtual と virtual が共存できます。

virtual 関数は override が必須

override を付けない場合、基底クラスの関数名を派生クラスでも使用できますが、両者は別の関数として扱われます。ただし派生クラス側で警告が出ます。基底クラスの関数がvirtualでない場合、意図的なものなら派生クラス側の関数名を変えろ(warning CS0108)、基底クラスの関数がvirtualの場合、派生クラスをoverrideにするか、意図的なら派生クラス側の関数名を変えろ(warning CS0114)と言われます。同じ関数名でも別の関数として扱われる仕組みは C++ でも名前修飾という仕組みがあり、何となく理解できました。

virtual なし override はできない

virtual が付いていない関数の override は許可されていません(error CS0506)。 Java は virtual が付いていない関数も virtual 扱いのため、override を付けるとオーバーライドできるようです(初めて知りました)が、C#では基底クラスに明示的に virtual が付いている関数しか override できません。

関数スコープは public

関数スコープは基底/派生クラスとも public にします。クラスの外からクラス関数にアクセスする場合、関数スコープは public 一択になります。尚、クラスの可視性は常にpublic(error CS1527)、virtual/overrideを使用する時のコードの可視性は、publicまたはprotectedが許可されています(error CS0621)。ただし、派生クラスの関数スコープは基底クラスと同じである必要があります(error CS0507)。

インターフェースを使用した場合

インターフェース(interface)を使用した場合は、virtual/overrideのような修飾子は不要ですが、実装に当たっていくつかの制限があります。

スコープ不要(C#)

インターフェース、およびインターフェースの関数宣言にスコープを付けません。インターフェースにpublic以外のスコープを付ける(error CS1527)、インターフェースの関数宣言にあらゆるスコープを付ける(error CS0106)と、コンパイルエラーになります。

インターフェースにpublicスコープが必要な場合がある(Unity)

Unityの場合、さらにインターフェースにpublicが付いていないと、インターフェースを引数として使う関数を実装できません(error CS0051)。また、実装型にインターフェース型を暗黙的に代入しようとするとコンパイルエラーになります(error CS0266)。

Unityで確認

// TestInterface.cs
public interface TestInterface
{
    // Unity では、interface に public を付けておく
    void test();
    void test_in(TestInterface it);
    TestInterface test_out();
}

// TestInterfaceInpliment.cs
using UnityEngine;

public class TestInterfaceInpliment : TestInterface
{
    TestInterfaceInpliment it_impl;
    
    public void test()
    {
        Debug.Log("test");
    }
    public void test_in(TestInterface it)
    {
        // interface が public でないと以下のエラーが出ます
        // error CS0051: Inconsistent accessibility: parameter type 'TestInterface' is less accessible than method 'TestInterfaceInpliment.test_in(TestInterface)'
        it_impl = (TestInterfaceInpliment)it;  // 実装型 ← インターフェース型(キャスト必要)
    }
    public TestInterface test_out()
    {
        return it_impl;  // インターフェース型 ← 実装型(暗黙的なキャスト)
    }
}

// TestInterfaceBehaviour.cs
using UnityEngine;

public class TestInterfaceBehaviour : MonoBehaviour
{
    TestInterface testInterface;
    
    public void Awake()
    {
        testInterface = new TestInterfaceInpliment();
        testInterface.test();
        testInterface.test_in(testInterface);
        testInterface = testInterface.test_out();
    }
}

Unityとしての制限

SerializeFieldが使えない

Unity で、インターフェース型を使う場合は、もう少し制限がありそうです(これはまだ分かっていない部分が多いです)。インターフェース型は、[SerializeField] で Inspector ウィンドウ から設定できるようにすることができません。いくつかのサイトを調べていると、Unity 2019.3 から インターフェース型もInspectorウィンドウに表示できる [SerializeReference] が追加されたとのことでした。しかし、他の方も指摘されているように、これを指定しても Inspector ウィンドウには参照名が出るだけで、ファイルをアタッチすることはできません。これについてはいくつかのサイトで設定方法が紹介されていました。そのうち調べようと思います。

確認コード

確認に使ったコードを載せておきます。

継承(virtual と override)

f:id:tomo_mana:20201012214155p:plain
virtual と override

ideone.comで確認

using System;

public class super_not_virtual {
    public void test()
    {
        Console.WriteLine("super_not_virtual\n");
    }
}
public class super_virtual {
    public virtual void test()
    {
        Console.WriteLine("super_virtual\n");
    }
//    public virtual void test2();  // error CS0501: `super_virtual.test2()' must have a body because it is not marked abstract, extern, or partial
}
public class sub_not_override_super_not_virtual : super_not_virtual {
    public void test()
    {
        Console.WriteLine("sub_not_override\n");  // warning CS0108: `sub_not_override_super_not_virtual.test()' hides inherited member `super_not_virtual.test()'. Use the new keyword if hiding was intended
    }
}
public class sub_not_override_super_virtual : super_virtual {
    public void test()
    {
        Console.WriteLine("sub_not_override\n");  // warning CS0114: `sub_not_override_super_virtual.test()' hides inherited member `super_virtual.test()'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword
    }
}
/*public class sub_override_super_not_virtual : super_not_virtual {
    public override void test()
    {
        Console.WriteLine("sub_override\n");  // error CS0506: `sub_override_super_not_virtual.test()': cannot override inherited member `super_not_virtual.test()' because it is not marked virtual, abstract or override
    }
}*/
public class sub_override_super_virtual : super_virtual {
    public override void test()
    {
        Console.WriteLine("sub_override\n");
    }
}
public class Test
{
    public static void Main()
    {
        // 1
        super_not_virtual snv1 = new sub_not_override_super_not_virtual();
        snv1.test();  // super_not_virtual
        
        // 2
        sub_not_override_super_not_virtual snosnv = new sub_not_override_super_not_virtual();
        snosnv.test();  // sub_not_override
        
        // 3
//      super_not_virtual snv2 = new sub_override_super_not_virtual();
//      snv2.test();  // compile error
        
        // 4
//      sub_override_super_not_virtual sosnv = new sub_override_super_not_virtual();
//      sosnv.test();  // compile error
        
        // 5
        super_virtual sv1 = new sub_not_override_super_virtual();
        sv1.test();  // super_virtual
        
        // 6
        sub_not_override_super_virtual snosv = new sub_not_override_super_virtual();
        snosv.test();  // sub_not_override
        
        // 7
        super_virtual sv2 = new sub_override_super_virtual();
        sv2.test();  // sub_override
        
        // 8
        sub_override_super_virtual sosv = new sub_override_super_virtual();
        sosv.test();  // sub_override
    }
}

継承(許可されたスコープ)

確認用コード(ideone.comで確認)

using System;

/*
protected class protected_super_virtual {
    // error CS1527: Namespace elements cannot be explicitly declared as private, protected, protected internal, or private protected
}
*/
public class super_virtual_public_func {
    public virtual void test()
    {
        Console.WriteLine("public_super_virtual\n");
    }
}
public class super_virtual_protected_func {
    protected virtual void test()
    {
        Console.WriteLine("public_super_virtual\n");
    }
}
/*
public class super_virtual_private_func {
    private virtual void test()
    {
        // error CS0621: `super_virtual_private_func.test()': virtual or abstract members cannot be private
        Console.WriteLine("public_super_virtual\n");
    }
}
*/
public class sub_override_public_func_super_virtual_public_func : super_virtual_public_func {
    public override void test()
    {
        Console.WriteLine("sub_override\n");
    }
}
/*
public class sub_override_public_func_super_virtual_protected_func : super_virtual_protected_func {
    public override void test()
    {
        // error CS0507: `sub_override_public_func_super_virtual_protected_func.test()': cannot change access modifiers when overriding `protected' inherited member `super_virtual_protected_func.test()'
        Console.WriteLine("sub_override\n");
    }
}
*/
public class sub_override_protected_func_super_virtual_protected_func : super_virtual_protected_func {
    protected override void test()
    {
        Console.WriteLine("sub_override\n");
    }
}
/*
public class sub_override_protected_func_super_virtual_public_func : super_virtual_public_func {
    protected override void test()
    {
        // error CS0507: `sub_override_protected_func_super_virtual_public_func.test()': cannot change access modifiers when overriding `public' inherited member `super_virtual_public_func.test()'
        Console.WriteLine("sub_override\n");
    }
}
*/

public class Test
{
    public static void Main()
    {
    }
}

インターフェース(C#

ideone.comで確認

using System;

interface ITestable {
//  public void test();  // error CS0106: The modifier `public' is not valid for this item
    void test();
    void test_in(ITestable it);
    ITestable test_out();
}
public interface ITestable_public {
    void test();
    void test_in(ITestable_public it);
    ITestable_public test_out();
}
class ITestable_implement : ITestable
{
    ITestable_implement it_impl;
    
    /*
    void test()
    {
        // error CS0737: `ITestable_implement' does not implement interface member `ITestable.test()' and the best implementing candidate `ITestable_implement.test()' is not public
        Console.WriteLine("test");  
    }
    */
    public void test()
    {
        Console.WriteLine("test");
    }
    public void test_in(ITestable it)
    {
        it_impl = (ITestable_implement)it;
    }
    public ITestable test_out()
    {
        ITestable it = it_impl;
        return it;
    }
}
class ITestable_implement_public : ITestable_public
{
    ITestable_implement_public it_impl;
    
    public void test()
    {
        Console.WriteLine("test");
    }
    public void test_in(ITestable_public it)
    {
        it_impl = (ITestable_implement_public)it;
    }
    public ITestable_public test_out()
    {
        ITestable_public it = it_impl;
        return it;
    }
}

/*
protected interface ITestable_protected {
    // error CS1527: Namespace elements cannot be explicitly declared as private, protected, protected internal, or private protected
    void test();
}
private interface ITestable_private {
    void test();
}
*/

public class Test
{
    public static void Main()
    {
        ITestable it = new ITestable_implement();
        it.test();
        it.test_in(it);
        it = it.test_out();
    }
}

インターフェース(Unityでの制限)

Unityで確認

// TestInterface.cs
public interface TestInterface
{
    void test();
    void test_in(TestInterface it);
    TestInterface test_out();
}

// TestInterfaceInpliment.cs
using UnityEngine;

public class TestInterfaceInpliment : TestInterface
{
    TestInterfaceInpliment it_impl;
    
    public void test()
    {
        Debug.Log("test");
    }
    public void test_in(TestInterface it)
    {
        // Unity では、interface が public でないと 以下のエラーが出る
        // error CS0051: Inconsistent accessibility: parameter type 'TestInterface' is less accessible than method 'TestInterfaceInpliment.test_in(TestInterface)'
        
//      it_impl = it;  // error CS0266: Cannot implicitly convert type 'TestInterface' to 'TestInterfaceInpliment'. An explicit conversion exists (are you missing a cast?)
        it_impl = (TestInterfaceInpliment)it;
    }
    public TestInterface test_out()
    {
        return it_impl;
    }
}

// TestInterfaceBehaviour.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TestInterfaceBehaviour : MonoBehaviour
{
    TestInterfaceInpliment testInterfaceInpliment;
    TestInterface testInterface;
    
    public void Awake()
    {
        testInterfaceInpliment = new TestInterfaceInpliment();
        testInterfaceInpliment.test();
        testInterfaceInpliment.test_in(testInterfaceInpliment);
//      testInterfaceInpliment = testInterfaceInpliment.test_out();  // error CS0266: Cannot implicitly convert type 'TestInterface' to 'TestInterfaceInpliment'. An explicit conversion exists (are you missing a cast?)
        testInterfaceInpliment = (TestInterfaceInpliment)testInterfaceInpliment.test_out();
        
        testInterface = new TestInterfaceInpliment();
        testInterface.test();
        testInterface.test_in(testInterface);
        testInterface = testInterface.test_out();
    }
}