WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

interface メンバーの「既定の実装」

by WebSurfer 2024年1月17日 14:54

C# 8.0 以降で、interface のメンバーに「既定の実装 (default implementation)」を設定できるようになり、それに関連してアクセス修飾子に private, protected, internal などを設定することが可能になりました。

ちなみに、C# 8.0 より前 (.NET Core 3 より前、.NET Framework はすべて) では interface のメンバーのアクセス修飾子は public しか許されておらず、継承する class 側でメンバーを実装する際にもアクセス修飾子に public と明示的に指定する必要がありました。

public 以外が許されなかった理由は、自分が調べたことの要約ですが、以下のようなことと理解しています。 (理由が明確に書いてある Microsoft のドキュメントは見つかりませんでした)

  • class が interface を継承すると、その class が必ず interface に定義されているメンバーを実装して公開するという外部との契約となる。(Interface の仕様に "An interface defines a contract. A class or struct that implements an interface shall adhere to its contract." と書いてあります)
  • 別の言い方をすると、そもそも interface に指定されるメンバーを実装して class を利用する外部に公開するのが目的なのに、private とか protected で隠ぺいするのは理にかなってない。

上記にもかかわらず、C# 8.0 以降で interface のメンバーに private や protected などを設定できるようになったのは何故か、その理由に興味があったので調べてみました。以下に調べたことを備忘録として書いておきます。

調べたことを簡単に書くと、C# 8.0 で interface に「既定の実装」という機能を追加する際に、ついでに public 以外のあらゆるアクセス修飾子を設定できるようにし、「既定の実装」に対するアクセスコントロールを可能にするというのが目的らしいです。

そのあたりのことが書いてあったドキュメントと、関係する部分の抜粋を以下に書いておきます。

  1. アクセス修飾子 (C# プログラミング ガイド) の「その他の型」
    "インターフェイス メンバー宣言には、あらゆるアクセス修飾子を含めることができます。 そのことは、クラスを実装するあらゆるもので必要になる共通実装を静的メソッドから与えるときに最も役に立ちます。Interface member declarations may include any access modifier. This is most useful for static methods to provide common implementations needed by all implementors of a class."
  2. interface (C# リファレンス)
    "インターフェイスによってメンバーの既定の実装を定義できます。 共通の機能を 1 回で実装する目的で static メンバーも定義できます。 An interface may define a default implementation for members. It may also define static members in order to provide a single implementation for common functionality."
  3. default interface methods
    "Add support for virtual extension methods - methods in interfaces with concrete implementations. A class or struct that implements such an interface is required to have a single most specific implementation for the interface method, either implemented by the class or struct, or inherited from its base classes or interfaces."
  4. インターフェイスのデフォルト実装
    "メソッド、プロパティ、インデクサー、イベントのアクセサーの実装を持てるようになった。アクセシビリティを明示的に指定できるようになった。静的メンバーを持てるようになった・・・中略・・・狭義にはこの1番目の機能こそが「デフォルト実装」です。 ただ、これのついでに実装されたものなので2番目、3番目には具体的な名前がついていません"

以下に、interface のメンバーを「既定の実装」とし、アクセス修飾子に public, private, protected, internal を使ったサンプルを載せておきます。説明はコード中にコメントとして書きましたのでそちらを見てください。

namespace ConsoleAppInterface
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Sample sample = new();
            ((ISampleDerived)sample).PublicMethod();
            sample.PublicMethod2();
            sample.Protected3();
            //((ISampleDerived)sample).Protected2();  // アクセス不可
        }
    }

    // C# 8 以降で、interface のメンバーに「既定の実装 (default
    // implementation)」を設定できるようになり、関連してアクセス修飾子に
    // private, protected, internal などを設定することが可能になった
    interface ISample
    {
        // デフォルトで public なのは以前と同じ。なので、アクセス修飾子を付
        // けない場合は public になる
        void Public()
        {
            Console.WriteLine("ISample.Public");
            Private();    // private メンバーにアクセス
        }

        internal void Internal()
        {
            Console.WriteLine("ISample.Internal");
        }

        protected void Protected()
        {
            Console.WriteLine("ISample.Protected");
        }

        // private の場合「既定の実装」は必須。無いと以下のエラー:
        // エラー CS0501 'ISample.Private()' は abstract、extern、または
        // partial に指定されていないため、本体を宣言する必要があります
        private void Private()
        {
            Console.WriteLine("ISample.Private");
        }
    }

    interface ISampleDerived : ISample
    {
        void PublicMethod()
        {
            // interface で interface を継承する場合、継承元の public, 
            // internal, protected メソッドを呼べる。「既定の実装」の有無
            // も関係なく呼べる
            Public();
            Internal();
            Protected();
            // Private();  private はもちろんダメ
        }

        void Default()
        {
            Console.WriteLine("ISampleDerived.Defualt");
        }

        // 派生先から protected メンバーにアクセスできるのは interface
        // だけ。class から呼ぶことはできない。呼ぶとエラーになる。下の
        // Sample の実装の PublicMethod2 メソッド内の説明を参照
        protected void Protected2()
        {
            Console.WriteLine("ISampleDerived.Protected2");
        }

        // 以下のような「既定の実装」がない場合、継承する class 側で実装が
        // 必要。ただし、継承する class 側では public にしないとエラー
        protected void Protected3();
    }

    public class Sample : ISampleDerived
    {
        // 継承元の ISampleDerived 内で「既定の実装」がされているメソッド
        // (この例では PublicMethod, Default, Protected2)は継承するクラ
        // スでの実装が無くてもエラーにならない

        public void PublicMethod2()
        {
            // Default を呼ぶには 1 段キャストが必要。単に Default(); とし
            // たのではエラー
            ((ISampleDerived)this).Default();

            // interface と違って class では protected なものは呼べない。

            //((ISampleDerived)this).Protected2();

            // ・・・とすると以下のエラーとなる:
            // エラー CS1540 'ISampleDerived' 型の修飾子をとおしてプロ
            // テクト メンバー 'ISampleDerived.Protected2()' にアクセスす
            // ることはできません。修飾子は 'Sample' 型、またはそれから派
            // 生したものでなければなりません
        }

        // ISampleDerived に「既定の実装」がない Protected3() があるので
        // class側で実装が必要。

        public void Protected3()
        {
            Console.WriteLine("Sample.Protected3");
        }

        // ただし、アクセス修飾子を public にしないと以下のエラー:
        // エラー CS0737 'Sample' は、インターフェイス メンバー
        // 'ISampleDerived.Protected3()' を実装していません。
        // 'Sample.Protected3()' は public ではないため、インターフェイス
        // メンバーを実装できません。

        // と言って class 側で Protected3 を実装しないと以下のエラー:
        // エラー CS0535 'Sample' はインターフェイス メンバー
        // 'ISampleDerived.Protected3()' を実装しません
    }
}

// 結果は:
// ISample.Public
// ISample.Private
// ISample.Internal
// ISample.Protected
// ISampleDerived.Defualt
// Sample.Protected3

Tags: , ,

.NET Framework

.NET Framework での Dependency Injection

by WebSurfer 2023年4月13日 15:12

ASP.NET Core で Dependency Injection (DI) に使われている Microsoft.Extensions.DependencyInjection 名前空間にあるクラス類は .NET Framework でもバージョン 4.6.1 以降であれば利用できるそうですので、.NET Framework 4.8 のコンソールアプリで試してみました。

先の記事「.NET Core での Dependency Injection」でターゲットフレームワーク .NET 5.0 のコンソールアプリに DI 機能を実装してみましたが、その .NET Framework 4.8 版です。

まず、Visual Studio 2022 のテンプレートを使ってターゲットフレームワーク .NET Framework 4.8 でコンソースアプリを作成し、NuGet から Microsoft.Extensions.DependencyInjection をインストールします。この記事を書いた時点での最新版 7.0.0 をインストールしました。

NuGet でインストール

他の Micosoft.Bcl.AsyncInterfaces などのパッケージは Microsoft.Extensions.DependencyInjection をインストールした時に同時に自動的にインストールされたものです。

検証に使ったコードは以下の通りです。先の記事「.NET Core での Dependency Injection」のものと同じです。

using Microsoft.Extensions.DependencyInjection;
using System;

namespace ConsoleAppDependencyInjection
{
    internal class Program
    {
        static void Main(string[] args)
        {
            IServiceCollection services = new ServiceCollection();

            services.AddTransient<IOrderRepository, SqlOrderRepository>();
            services.AddSingleton<ILogger, Logger>();
            services.AddScoped<IEventPublisher, EventPublisher>();
            services.AddTransient<CancelOrderHandler>();

            var provider = services.BuildServiceProvider();
            var handler = provider.GetRequiredService<CancelOrderHandler>();

            var orderId = Guid.NewGuid();
            var command = new Order { OrderId = orderId };
            handler.Handle(command);
        }
    }

    public class CancelOrderHandler
    {
        private readonly IOrderRepository repository;
        private readonly ILogger logger;
        private readonly IEventPublisher publisher;

        // Use constructor injection for the dependencies
        public CancelOrderHandler(IOrderRepository repository,
                                  ILogger logger,
                                  IEventPublisher publisher)
        {
            this.repository = repository;
            this.logger = logger;
            this.publisher = publisher;
        }

        public void Handle(Order command)
        {
            this.logger.Log($"Cancelling order {command.OrderId}");
            var order = this.repository.GetById(command.OrderId);
            order.OrderStatus = "Cancelled";
            this.repository.Save(order);
            this.publisher.Publish(order);
        }
    }

    public interface IOrderRepository
    {
        Order GetById(Guid orderId);

        void Save(Order order);
    }

    public class SqlOrderRepository : IOrderRepository
    {
        private readonly ILogger logger;

        // Use constructor injection for the dependencies
        public SqlOrderRepository(ILogger logger)
        {
            this.logger = logger;
        }

        public Order GetById(Guid orderId)
        {
            this.logger.Log($"Getting Order {orderId}");

            // Retrieve from db.・・・のつもり
            var order = new Order
            {
                OrderId = orderId,
                ProductName = "911-GT3",
                OrderStatus = "Ordered"
            };

            return order;
        }

        public void Save(Order order)
        {
            this.logger.Log($"Saving order {order.OrderId}");
            // Save to db.
        }
    }

    public interface ILogger
    {
        void Log(string log);
    }

    public class Logger : ILogger
    {
        public void Log(string log)
        {
            Console.WriteLine(log);
        }
    }

    public interface IEventPublisher
    {
        void Publish(Order order);
    }

    public class EventPublisher : IEventPublisher
    {
        public void Publish(Order order)
        {
            Console.WriteLine($"Publish order {order.OrderId}, " +
                $"{order.ProductName}, {order.OrderStatus}");
        }
    }

    public class Order
    {
        public Guid OrderId { get; set; }

        public string ProductName { get; set; }

        public string OrderStatus { get; set; }
    }
}

実行結果は以下の画像のようになります。先の記事の .NET 5.0 版と同様に期待した結果になっています。

実行結果

Tags: ,

.NET Framework

異種の要素を含む JSON 配列のデシリアライズ

by WebSurfer 2023年3月5日 13:14

JSON の配列で string, number, true/false, null, array, object など異種の要素が混ざったものを、デシリアライザに Newtonsoft.Json および System.Text.Json を使って .NET のオブジェクトに変換するとどうなるかを書きます。

デシリアライズ結果

上の画像の前者 result1.Data が Newtonsoft.Json を使った結果、後者 result2.Data が System.Text.Json を使った結果です。

Data は両方とも object[] 型で、その各要素も object 型ですが、その中身が違うところに注目してください。それがこの記事で書きたかったことです。

この例ほど多種の要素が混じっている配列というケースは実際にはないかもしれませんが、"NaN" という文字列と number ぐらいならあるかもしれません。なので、違いを覚えておくと役に立つかもしれないと思って備忘録として残しておくことにしました。

上の画像を表示するのに具体的のどのようにしたかを以下に書いておきます。

まず、元になる JSON 文字列ですが、以下の通りです。

{
  "Header": {
    "Id": 10,
    "Name": "Test Data"
  },
  "Data": [
    "NaN",
    -0.010565,
    true,
    10,
    1.02e2,
    null,
    [ "abc", "def", "ghi" ],
    { "Object": "test" }
  ]
}

Visual Studio の機能を使って上の JSON 文字列から C# のクラス定義を生成すると以下のようになります。やり方は先の記事「JSON 文字列から C# のクラス定義生成」を見てください。

public class Rootobject
{
    public Header Header { get; set; }
    public object[] Data { get; set; }
}

public class Header
{
    public int Id { get; set; }
    public string Name { get; set; }
}

"Data" は object[] 型にデシリアライズするという点に注目してください。

Newtonsoft.Json および System.Text.Json を使って JSON 文字列を上の C# のオブジェクトにデシリアライズします。コードは以下の通りです。ターゲットフレームワーク .NET 7.0 のコンソールアプリを使いました。

string filepath = @"C:\Users\...\json1.json";
string jsonString = "";
using (StreamReader sr = File.OpenText(filepath))
{
    jsonString = sr.ReadToEnd();
}

// Newtonsoft.Json
var result1 = Newtonsoft.Json.JsonConvert
              .DeserializeObject<Rootobject>(jsonString);

// System.Text.Json
var result2 = System.Text.Json.JsonSerializer
              .Deserialize<Rootobject>(jsonString);

上のコードを Visual Studio 2022 でデバッグ実行し、ブレークポイントで止めて変数 result1 と result2 の中身を表示したのがこの記事の一番上の画像です。

Tags: , ,

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar