20210118のC#に関する記事は6件です。

超入門!C#のクラスとインスタンス

クラス

c#におけるクラスとは、簡単に言うと、各々で定義する処理を目的や種類別にまとめておける仕組みで、ものづくりの設計図によく例えられます。

  • どの変数が、何を記録しているのか?
  • どのメソッドが、どんな処理を行うのか?

といった事がわかりやすくなるメリットがあります。
実際のクラス定義は以下の様に行います。

class Actor{
    string name;
}

これで、string型のnameという変数を持ったクラスを宣言できました。
変数nameの様に、クラスが持つ変数を「メンバー変数」と呼びます。
今回の場合、メンバー変数を一緒に宣言する事で、nameという情報を持ったActorの設計図と作ったことになります。

また、C#において、クラスは絶対的なルールとして扱われます。

その為、クラスを作らずに変数やメソッドを定義する事はできません。
この様に変数やメソッドをはじめとする要素それぞれが目的を持ち、グループ分けされる様な構造の言語をオブジェクト指向言語と言います。

オブジェクト指向に関しては、例えばC#で、Unityなどのゲームエンジンで開発する際は、特にわかりやすいかもしれません。
ゲーム上では、シーン上に出現する物体を、プレイヤーキャラクターや、敵キャラクター、背景、動作、音楽の様な、各々の目的と特性を持ったオブジェクトとして扱う為、役割が明確ですね。

インスタンス

さて、クラスというグループを作り、物を作る設計図ができました。
この中に実際のボタンやキャラクターなどのオブジェクトを流し込んでいきます。

ここで、インスタンスという物を説明します。
一言で言うと、キャラクターの様な、クラスを元に作られた実体がインスタンスです。
具体的には、new演算子を用いて以下の様に生成します。

Actor act = new Actor();

何をやっているのかを解説すると、以下の様になります。

クラス名 インスタンス名 = new クラス名();

ここで、C#における変数の宣言方法を思い出してください。

型 変数名 = 値;

というコードで変数に値を代入するというルールがあります。
これはインスタンスを生成する際にも同様で、
new演算子を用いてインスタンス生成する際の、最初のクラス名は、データ型に相当します。

つまり、何が起きているのかというと
クラスを生成すると、そのクラスに対応した新しい型が出来上がるのです。

その為、インスタンスを生成すると、「クラス生成時に同時に生まれた新しい型」の変数に
「新しいインスタンス」が値として代入されます。

ここまでで、クラスの宣言と、からのインスタンスが生成されました。
クラスと一緒に、メンバー変数も宣言していますので、メンバー変数の中身も代入しましょう。

先ほどインスタンスを代入した変数actに対して、以下の様にするとメンバー変数nameに値を代入できます。

act.name = "太郎";

これで、Actorクラスから太郎というnameを持ったインスタンスが出来上がりました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【C#における変数と型変換(キャスト)】

変数の宣言と代入

C#では、変数に値を代入する際に、変数名の前にデータ型の種類を記載する必要があります。

int x = 123;
float y = 42.195f;

この場合、変数xに123というint型の値を代入しています。

なお、実数を扱うfloat型の場合、値の末尾にfを記述し、値がfloat型である事をさらに明記します。
(実数を扱う別の型である'double型'等と明確に区別する為)
末尾の記述はデータ型ごとにルールがあるので、都度、調べてみてください。

また、今回の様に宣言と代入を同時に行う際には”変数の初期化”と呼びます。

変数の型変換(キャスト)

変数に値を代入する際には、異なる型の値を代入することはできません。
その為、以下の様なコードではエラーが発生します。

(誤った例)
int y = 42.195f;

このエラーを解消するために用いるのが、” 型変換(別名:キャスト) ” です。
型変換により、異なる型の値を別の型に変換して代入できます。

実際の型変換のコードは以下の通りです。

(正しい例)
int z = (int)42.195f;

または、
float y = 42.195f;
int z = (int)y;

これで、float型で扱うべき値をint型に変換して、変数zに代入することができました。
(int型として宣言された変数zを呼び出すと、小数点以下は切り捨てられますが、これはエラーではありません。)

また、型変換はどんな型同士でも可能というわけではありません。
数値同士の型変換など、関連性のある型同士に限りますので注意が必要です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LINQのSelectは「遅延」するので使い方に気をつけるべし

LINQのSelect関数は、配列などのIEnumerable<T>オブジェクトの要素一つ一つについて、指定した処理を施した結果を別のIEnumerable<T>として返してくれる。

using System.Collections.Generic;
using System.Linq;

void Func() {
    var list = new List<int>{1, 2, 3, 4, 5};

    foreach (var item in list.Select(i => i * i)) {
        System.Console.WriteLine(item); // 1, 4, 9, 16, 25 が1行ずつで表示される
    }
}

見落としがちなのが、このSelectで施そうとする処理は、実際に結果となるイテレート可能なオブジェクトが利用されるまで、実行が遅れるということである。

void Func() {
    var list = new List<int>{1, 2, 3, 4, 5};

    var sqList = list.Select(i => i * i); // この時点で、i * i は「計算されていない」
}

この例だと、Selectで作ったものはただの数値なのでSelectの実行が遅れてもさほど問題にはならないだろう。

問題になるのは、要素からクラスを作ろうとする時だ。

バグにつながる使い方

この例はUnity3D (UniTask) での利用をあげているが、そのほかのasyncのやり方でも同じことが考えられる。

あるクラスをnewするときに重い処理が必要なので、コンストラクタからasyncコンテクストでその重い処理を行わせる指示だけをしておいて、準備ができたらメンバ変数のフラグをtrueにすることを考えよう。そして、そのクラスを別の配列の値からSelectを経由して作り、全てのクラスインスタンスから「準備完了」が返ってくるのを待つ処理を考える。

インターネット経由で指定したURLにある大きなデータを取ってくるという目的で、上記のような実装を考えるとこういったものになるだろう:

public class FetchBigData {
    private bool _isReadyToRead = false;
    public bool IsReadyToRead { get {
            return _isReadyToRead;
        } 
    };

    private object _data;
    public object Data { get {
            return _data;
        }
    };

    public FetchBigData(string url) {
        FetchURL(url).Forget();
    }

    private async UniTask FetchURL(string url) {
        _data = await ...... // ダウンロード処理
        _isReadyToRead = true;
    }
}

そして、このクラスを複数のURLで作ってこのように処理完了を待つことを考える。

void LoadManyGoneWrong() {
    var urls = new List<string>{"url1", "url2", "url3"};
    var fetchBigDataList = urls.Select(url => new FetchBigData(url)); // Uh oh

    // NG!!!
    await UniTask.WaitUntil(() => fetchBigDataList.All(i => i.IsReadyToLoad)); // 条件がfalseの間await
    foreach (var data in fetchBigDataList.Select(i => i.Data)) {
        // Do something about data
    }
}

残念ながら、この実装は await でソフトロックする。そればかりか、おそらくサーバーに大量のリクエストが飛んで怒られる羽目になる。

その理由は、Selectの実行が遅れることにある。fetchBigDataListを作ったSelectでは、URLをFetchBigDataのコンストラクタに流してFetchBigDataの配列を作ることになっている。この処理の実行が遅れているのだ。つまり、この時点でFetchBigDataは作成されていない

遅れた処理は、それによって出来上がる要素が必要とされるまで実行されない。WaitUntilを評価しようとしてAllを実行して初めてSelectが作る配列の中身が必要になるため、Selectの中身が実行されてFetchBigDataが作成され、コンストラクタで呼んだダウンロード処理が発生する。

ちなみに、UniTask.WaitUntilはそれに与えた条件がtrueになるまで、その条件を毎フレーム評価し続ける。ということは、Alltrueを返すまで、Allは呼ばれ続ける。

都合の悪いことに、これはSelectの中身まで毎フレーム呼ばれることを意味する。つまりawaitに実行が進んだ時点で、大量のFetchBigDataが作られ続け、その時点でのダウンロード完了状態 == false が確認され、いつまで経っても実行が進まなくなる ばかりか FetchBigDataからのリクエストが飛び続ける事故が発生する。

回避方法

ではどうすればいいのかというと、Allを打つ前にSelectが作る配列の中身が必要な処理を実行してやれば良いのである。例えばTolistだ。

void CorrectlyLoadMany() {
    var urls = new List<string>{"url1", "url2", "url3"};
    var fetchBigDataList = urls.Select(url => new FetchBigData(url)).ToList(); // ToList()を打ち、強制的にSelectを実行させる

    await UniTask.WaitUntil(() => fetchBigDataList.All(i => i.IsReadyToLoad)); // 条件がfalseの間await
    foreach (var data in fetchBigDataList.Select(i => i.Data)) {
        // Do something about data
    }
}

このようにすれば、最初のSelectで生成したFetchBigDataがその後も使い続けられ、無事に正しい数のリクエストが飛ぶこととなる。

Select以外も遅延する

今回はSelectを使ってハマったが、コメントでの指摘の通り、Selectに限らずIEnumerable<T>が戻り値の型になっているLINQメソッドには同じような遅延実行が起こる(e.g. Where GroupBy Take Skip)。実は公式ドキュメントの注釈にも書かれている。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

KeyTrigger に紐づけたコマンドが何回も発報する

困りごと

私のための備忘録です。

以下のように KeyTrigger とコマンドを紐づけたコントロールを用意し、それを TabControl->TabItem に置いてみたところ、掲題のようにコマンドが何回も発報する現象に遭遇。

正確に言うと、タブの選択状態が切り替わるたびに KeyTrigger とコマンドの紐づけが再生成されているようで、切り替えた回数だけコマンドが呼び出されていました。

Region
<UserControl ...
             xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
             xmlns:p="http://prismlibrary.com/">
    <ComboBox ItemsSource="{Binding XXX, Mode=OneWay}"
              Text="{Binding YYY, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
              IsReadOnly="{Binding ZZZ, Mode=OneWay}"
              IsEditable="True">
        <i:Interaction.Triggers>
            <i:KeyTrigger ActiveOnFocus="True" Key="Return">
                <p:InvokeCommandAction Command="{Binding HogeCommand, Mode=OneTime}"/>
            </i:KeyTrigger>
        </i:Interaction.Triggers>
    </ComboBox>
</UserControl>
Window
<Window ...
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:p="http://prismlibrary.com/">
    <Window.Resources>
        <Style x:Key="RegionContent" TargetType="{x:Type ContentControl}">
            <Setter Property="p:RegionManager.RegionManager" Value="{Binding RegionManager, RelativeSource={RelativeSource AncestorType={x:Type Window}}, Mode=OneWay}"/>
        </Style>
    </Window.Resources>
    <TabControl>
        <TabItem Header="タブ1">
            <ContentControl Style="{StaticResource RegionContent}" p:RegionManager.RegionName="Tab1Region"/>
        </TabItem>
        <TabItem Header="タブ2">
            <ContentControl Style="{StaticResource RegionContent}" p:RegionManager.RegionName="Tab2Region"/>
        </TabItem>
    </TabControl>
</Window>

手がかり

かなり前ですが、同じような現象で困っていた方を発見。
どうやら Blend 時代からの仕様らしく、KeyTrigger を使わずに対応する必要がありそうです。

stack overflow: Blend KeyTrigger fires multiple times

対応策

InputBindings に変えたらこの現象は収ました。

Region
<UserControl ...
             xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
             xmlns:p="http://prismlibrary.com/">
    <ComboBox ItemsSource="{Binding XXX, Mode=OneWay}"
              Text="{Binding YYY, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
              IsReadOnly="{Binding ZZZ, Mode=OneWay}"
              IsEditable="True">
        <ComboBox.InputBindings>
            <KeyBinding Command="{Binding HogeCommand, Mode=OneTime}" Key="Return"/>
        </ComboBox.InputBindings>
    </ComboBox>
</UserControl>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プラグインのサンプルを書いてC#のクラスローディングを理解する

Azure Functions ホストの実装は クラスローディングの塊なので、そろそろ仕組みをじっくり理解したくなってきた。そのために、その基礎のクラスの使い方を理解していきたい。今日はまず Create a .NET Core application with pluginsのチュートリアルを理解しながら流していきたい。

プロジェクトのひな型を作る

C#のものにしては珍しく、コマンドラインから作成するようになっている。
折角なのでコマンドを調べてみよう。

dotnet new -l
Templates                                         Short Name               Language          Tags
--------------------------------------------      -------------------      ------------      ----------------------
Console Application                               console                  [C#], F#, VB      Common/Console
Class library                                     classlib                 [C#], F#, VB      Common/Library
WPF Application                                   wpf                      [C#], VB          Common/WPF
WPF Class library                                 wpflib                   [C#], VB          Common/WPF
WPF Custom Control Library                        wpfcustomcontrollib      [C#], VB          Common/WPF
WPF User Control Library                          wpfusercontrollib        [C#], VB          Common/WPF
Windows Forms App                                 winforms                 [C#], VB          Common/WinForms
Windows Forms Control Library                     winformscontrollib       [C#], VB          Common/WinForms
Windows Forms Class Library                       winformslib              [C#], VB          Common/WinForms
Worker Service                                    worker                   [C#], F#          Common/Worker/Web
Unit Test Project                                 mstest                   [C#], F#, VB      Test/MSTest
NUnit 3 Test Project                              nunit                    [C#], F#, VB      Test/NUnit
NUnit 3 Test Item                                 nunit-test               [C#], F#, VB      Test/NUnit
xUnit Test Project                                xunit                    [C#], F#, VB      Test/xUnit
Razor Component                                   razorcomponent           [C#]              Web/ASP.NET
Razor Page                                        page                     [C#]              Web/ASP.NET
MVC ViewImports                                   viewimports              [C#]              Web/ASP.NET
MVC ViewStart                                     viewstart                [C#]              Web/ASP.NET
Blazor Server App                                 blazorserver             [C#]              Web/Blazor
Blazor WebAssembly App                            blazorwasm               [C#]              Web/Blazor/WebAssembly
ASP.NET Core Empty                                web                      [C#], F#          Web/Empty
ASP.NET Core Web App (Model-View-Controller)      mvc                      [C#], F#          Web/MVC
ASP.NET Core Web App                              webapp                   [C#]              Web/MVC/Razor Pages
ASP.NET Core with Angular                         angular                  [C#]              Web/MVC/SPA
ASP.NET Core with React.js                        react                    [C#]              Web/MVC/SPA
ASP.NET Core with React.js and Redux              reactredux               [C#]              Web/MVC/SPA
Razor Class Library                               razorclasslib            [C#]              Web/Razor/Library
ASP.NET Core Web API                              webapi                   [C#], F#          Web/WebAPI
ASP.NET Core gRPC Service                         grpc                     [C#]              Web/gRPC
dotnet gitignore file                             gitignore                                  Config
global.json file                                  globaljson                                 Config
NuGet Config                                      nugetconfig                                Config
Dotnet local tool manifest file                   tool-manifest                              Config
Web Config                                        webconfig                                  Config
Solution File                                     sln                                        Solution
Protocol Buffer File                              proto                                      Web/gRPC

結構なテンプレートが使える感じだ。VSに出てくるのと近い感じだろうか。自動化するときに便利そう。早速つくってみる。-o オプションは、アウトプットを示すとヘルプに書いてあるが、ディレクトリがその名前で作成される。

> dotnet new console -o AppWithPlugin
The template "Console Application" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on AppWithPlugin\AppWithPlugin.csproj...
  Determining projects to restore...
  Restored C:\Users\tsushi\Code\NET\DependencyLoading\AppWithPlugin\AppWithPlugin.csproj (in 50 ms).
Restore succeeded.

ごく基本的なアプリケーションが生成されている様子。
image.png

今作ると、net5 になる感じ。

AppWithPlugin.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

</Project>

VisualStudio で使えるようにソリューションファイルを作成する。

> dotnet new sln
The template "Solution File" was created successfully.

次のような sln ファイルが出来上がる。

DependencyLoading.sln

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Global
    GlobalSection(SolutionConfigurationPlatforms) = preSolution
        Debug|Any CPU = Debug|Any CPU
        Debug|x64 = Debug|x64
        Debug|x86 = Debug|x86
        Release|Any CPU = Release|Any CPU
        Release|x64 = Release|x64
        Release|x86 = Release|x86
    EndGlobalSection
    GlobalSection(SolutionProperties) = preSolution
        HideSolutionNode = FALSE
    EndGlobalSection
EndGlobal

csproj ファイルを ソリューションファイルに追加する

dotnet sln add AppWithPlugin\AppWithPlugin.csproj
Project `AppWithPlugin\AppWithPlugin.csproj` added to the solution.

sln ファイルに、 次の箇所が追加されている。

Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppWithPlugin", "AppWithPlugin\AppWithPlugin.csproj", "{EF89D6BC-1C8C-4CEA-81C4-B0F89D5FCCE5}"
    GlobalSection(ProjectConfigurationPlatforms) = postSolution
        {EF89D6BC-1C8C-4CEA-81C4-B0F89D5FCCE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {EF89D6BC-1C8C-4CEA-81C4-B0F89D5FCCE5}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {EF89D6BC-1C8C-4CEA-81C4-B0F89D5FCCE5}.Debug|x64.ActiveCfg = Debug|Any CPU
        {EF89D6BC-1C8C-4CEA-81C4-B0F89D5FCCE5}.Debug|x64.Build.0 = Debug|Any CPU
        {EF89D6BC-1C8C-4CEA-81C4-B0F89D5FCCE5}.Debug|x86.ActiveCfg = Debug|Any CPU
        {EF89D6BC-1C8C-4CEA-81C4-B0F89D5FCCE5}.Debug|x86.Build.0 = Debug|Any CPU
        {EF89D6BC-1C8C-4CEA-81C4-B0F89D5FCCE5}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {EF89D6BC-1C8C-4CEA-81C4-B0F89D5FCCE5}.Release|Any CPU.Build.0 = Release|Any CPU
        {EF89D6BC-1C8C-4CEA-81C4-B0F89D5FCCE5}.Release|x64.ActiveCfg = Release|Any CPU
        {EF89D6BC-1C8C-4CEA-81C4-B0F89D5FCCE5}.Release|x64.Build.0 = Release|Any CPU
        {EF89D6BC-1C8C-4CEA-81C4-B0F89D5FCCE5}.Release|x86.ActiveCfg = Release|Any CPU
        {EF89D6BC-1C8C-4CEA-81C4-B0F89D5FCCE5}.Release|x86.Build.0 = Release|Any CPU
    EndGlobalSection

上記の作業が済むと、slnファイルに csproj ファイルが認識されるので、VS から開いてみる。

Main コマンドラインパーサー

Main のプログラムを書く。これはコマンドラインパーサーのようだ。

   class Program
    {
        static void Main(string[] args)
        {
            try
            {
                if (args.Length == 1 && args[0] == "/d")
                {
                    Console.WriteLine("Waiting for any key...");
                    Console.ReadLine();
                }

                // Load commands from plugins 
                if (args.Length == 0)
                {
                    Console.WriteLine("Commands: ");
                } 
                else
                {
                    foreach (string commandName in args)
                    {
                        Console.WriteLine($"-- {commandName} --");
                        Console.WriteLine();
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }
    }

プラグインインターフェイスの作成

dotnet new classlib -o PluginBase
dotnet sln add PluginBase/PluginBase.csproj

プラグインが実装すべきインターフェイスを作成しておく。

ICommand.cs

namespace PluginBase
{
    public interface ICommand
    {
        string Name { get; }
        string Description { get; }
        int Execute();
    }
}

AppWithPlugin のプロジェクトが、このインターフェイスのプロジェクトを参照できるようにする。

dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj

コマンドの実体は、AppWithPlugin.csproj のファイルに

  <ItemGroup>
    <ProjectReference Include="..\PluginBase\PluginBase.csproj" />
  </ItemGroup>

が追加されている。
VSで見ると次のイメージ

image.png

クラスローダーの実装

AssemblyLoadContext

アセンブリローディングのコンテキストオブジェクトです。これが複数あると、同じライブラリの別バージョンもロードすることが可能です。カスタムの AssemblyLoadContext を作りたいときは、自分でオーバーライドして作成します。

今回は AssemblyDependencyResolver という依存性解決のためのクラスを使っています。ResolveAssemblyToPath により、アセンブリ名から、アセンブリの存在するパスを取得しています。また、ResolveUnmanagedDllToPath によって、deps.json に載っている、ネイティブライブラリをロードします。戻りが、IntPtr になっているのはよくわかっていないので継続して調査が必要です。

** NOTE:** UnmanagedDll ネイティブライブラリロードの戻り値はなぜ IntPtr なのだろう?

  public class PluginLoadContext : AssemblyLoadContext
    {
        private AssemblyDependencyResolver _resolver;

        public PluginLoadContext(string pluginPath)
        {
            _resolver = new AssemblyDependencyResolver(pluginPath);
        }

        protected override Assembly Load(AssemblyName assemblyName)
        {
            string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
            if (assemblyPath != null)
            {
                return LoadFromAssemblyPath(assemblyPath);
            }
            return null;
        }

        protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
        {
            string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
            if (libraryPath != null)
            {
                return LoadUnmanagedDllFromPath(libraryPath);
            }
            return IntPtr.Zero;
        }
    }

プラグインのロード

こちらは、typeof(Program).Assembly.Location によって、Program クラスが所属するアセンブリ (DLL) の場所を戻している。ちなみに Path はクロスプラットフォーム用のライブラリで、Path.GetDirecotryName はディレクトリ名を取得している。
PluginContext を取得して、アセンブリをそこから取得している。

        static Assembly LoadPlugin(string relativePath)
        {
            string root = Path.GetFullPath(Path.Combine(
                Path.GetDirectoryName(
                    Path.GetDirectoryName(
                        Path.GetDirectoryName(
                            Path.GetDirectoryName(
                                Path.GetDirectoryName(typeof(Program).Assembly.Location)))))));

            string pluginLocation = Path.GetFullPath(Path.Combine(root, relativePath.Replace('\\', Path.DirectorySeparatorChar)));
            Console.WriteLine($"Loading commands from: {pluginLocation}");
            PluginLoadContext loadContext = new PluginLoadContext(pluginLocation);
            return loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(pluginLocation)));

        }

余談 Path 系メソッド

最初のパートはなぜそのようなコードになっているのかよくわからなかった。ぼんやりと、sln のいるプロジェクトルートを取得しているのはわかるが、Path.GetFullPathPath.Combine の存在意義がよくわからない。

ちなみに、こんなサンプルコードを書いて、Win10 と Linux (WSL2 ubuntu) でテストしてみた。

Code

            string root = Path.GetFullPath(Path.Combine(
                Path.GetDirectoryName(
                    Path.GetDirectoryName(
                        Path.GetDirectoryName(
                            Path.GetDirectoryName(
                                Path.GetDirectoryName(typeof(Program).Assembly.Location)))))));
            string rootWithoutCombine = Path.GetFullPath(
                Path.GetDirectoryName(
                    Path.GetDirectoryName(
                        Path.GetDirectoryName(
                            Path.GetDirectoryName(
                                Path.GetDirectoryName(typeof(Program).Assembly.Location))))));
            string rootWithoutCombineAndGetFullPath = 
                Path.GetDirectoryName(
                    Path.GetDirectoryName(
                        Path.GetDirectoryName(
                            Path.GetDirectoryName(
                                Path.GetDirectoryName(typeof(Program).Assembly.Location)))));
            Console.WriteLine(root);
            Console.WriteLine(rootWithoutCombine);
            Console.WriteLine(rootWithoutCombineAndGetFullPath);
            Console.WriteLine(typeof(Program).Assembly.Location);
            Console.WriteLine(Path.GetDirectoryName(typeof(Program).Assembly.Location));
            Console.WriteLine(Path.GetDirectoryName(Path.GetDirectoryName(typeof(Program).Assembly.Location)));

Windows

C:\Users\tsushi\Code\NET\DependencyLoading
C:\Users\tsushi\Code\NET\DependencyLoading
C:\Users\tsushi\Code\NET\DependencyLoading
C:\Users\tsushi\Code\NET\DependencyLoading\ClassLibrarySpike\bin\Debug\netcoreapp3.1\ClassLibrarySpike.dll
C:\Users\tsushi\Code\NET\DependencyLoading\ClassLibrarySpike\bin\Debug\netcoreapp3.1
C:\Users\tsushi\Code\NET\DependencyLoading\ClassLibrarySpike\bin\Debug

Linux

/home/ushio/Code/NET/DependencyLoading
/home/ushio/Code/NET/DependencyLoading
/home/ushio/Code/NET/DependencyLoading
/home/ushio/Code/NET/DependencyLoading/ClassLibrarySpike/bin/Debug/netcoreapp3.1/ClassLibrarySpike.dll
/home/ushio/Code/NET/DependencyLoading/ClassLibrarySpike/bin/Debug/netcoreapp3.1
/home/ushio/Code/NET/DependencyLoading/ClassLibrarySpike/bin/Debug

何が違うのか?

パスのデリミタなどはうまく対応できている。自分でパスをプラとフォームに合わせてめんどくさいコードを書く必要はない。ただ、GetFullPathPath.Combine がある意図がよくわからない。なんでやろ。ドキュメントのサンプルのミス(もともと違うコードで、複数の文字を Combine してたなど。

プラグインの作成

プロジェクトの作成

dotnet new classlib -o HelloPlugin
dotnet sln add HelloPlugin/HelloPlugin.csproj

csproj ファイル

Privateは重要で、この設定だと、PluginBaseプロジェクトを参照していますが、この設定が無いと、ここでビルドしたディレクトリ(outputフォルダ)の配下にPluginBase.dll が作成されます。そうなると、PluginContext がそのアウトプットディレクトリからアセンブリをロードしてしまいます。ですので、HelloPlugin.HelloCommand の実装する ICommandHelloPlugin の output ディレクトリの ICommand の実装となります。AppWithPlugin のデフォルトコンテキストからロードされたものになりません。

ExcludeAssets の設定に関しては、このサンプルに限っては挙動は同じなのですが、プロジェクトが複雑になって、 PluginBase がPackageReferenceを持っている場合、false が参照の方に伝播しないので、そのワークアラウンドとして、設定が必要になります。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\PluginBase\PluginBase.csproj">
      <Private>false</Private>
      <ExcludeAssets>runtime</ExcludeAssets>
    </ProjectReference>
  </ItemGroup>

</Project>

プラグイン本体

めっちゃ単純です。

HelloCommand.cs

    public class HelloCommand : ICommand
    {
        public string Name { get => "hello"; }

        public string Description { get => "Displays hello message.";  }

        public int Execute()
        {
            Console.WriteLine("Hello !!!");
            return 0;
        }
    }
}

Package reference の挙動の違い

Private = false

image.png

HelloPlugin.deps.json

{
  "runtimeTarget": {
    "name": ".NETCoreApp,Version=v5.0",
    "signature": ""
  },
  "compilationOptions": {},
  "targets": {
    ".NETCoreApp,Version=v5.0": {
      "HelloPlugin/1.0.0": {
        "dependencies": {
          "PluginBase": "1.0.0"
        },
        "runtime": {
          "HelloPlugin.dll": {}
        }
      }
    }
  },
  "libraries": {
    "HelloPlugin/1.0.0": {
      "type": "project",
      "serviceable": false,
      "sha512": ""
    }
  }
}

Private = false なし

image.png

HelloPlugin.deps.json

{
  "runtimeTarget": {
    "name": ".NETCoreApp,Version=v5.0",
    "signature": ""
  },
  "compilationOptions": {},
  "targets": {
    ".NETCoreApp,Version=v5.0": {
      "HelloPlugin/1.0.0": {
        "dependencies": {
          "PluginBase": "1.0.0"
        },
        "runtime": {
          "HelloPlugin.dll": {}
        }
      },
      "PluginBase/1.0.0": {
        "runtime": {
          "PluginBase.dll": {}
        }
      }
    }
  },
  "libraries": {
    "HelloPlugin/1.0.0": {
      "type": "project",
      "serviceable": false,
      "sha512": ""
    },
    "PluginBase/1.0.0": {
      "type": "project",
      "serviceable": false,
      "sha512": ""
    }
  }
}

実行

パラメータや、実行ファイルの場所が記載されていなかったので、追加。パラメータで、読み込むアセンブリと、Plugin の名前を指定するようにした。

    static void Main(string[] args)
        {
            try
            {
                if (args.Length == 1 && args[0] == "/d")
                {
                    Console.WriteLine("Waiting for any key...");
                    Console.ReadLine();
                }

                string[] pluginPaths = new string[]
                {
                    // Paths to plugins to load.
                    args[0]
                };

                IEnumerable<ICommand> commands = pluginPaths.SelectMany(pluginPath =>
                {
                    Assembly pluginAssembly = LoadPlugin(pluginPath);
                    return CreateCommands(pluginAssembly);
                }).ToList();
                if (args.Length == 0)
                {
                    Console.WriteLine("Commands: ");
                    // Output the loaded commands.
                    foreach (ICommand command in commands)
                    {
                        Console.WriteLine($"{command.Name}\t - {command.Description}");
                    }
                }
                else
                {
                    foreach (string commandName in args.Skip(1))
                    {
                        Console.WriteLine($"-- {commandName} --");
                        // Execute the command with the name passed as an argument.
                        ICommand command = commands.FirstOrDefault(command => command.Name == commandName);
                        if (command == null)
                        {
                            Console.WriteLine("No such command is known.");
                            return;
                        }

                        command.Execute();

                        Console.WriteLine();
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

image.png

Loading commands from: C:\Users\tsushi\Code\NET\DependencyLoading\HelloPlugin\bin\Debug\net5.0\HelloPlugin.dll
-- hello --
Hello !!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityのAsyncOperation.isDoneでtrueになるやつとならないやつ

前提

タイトルで、「trueになるやつとならないやつ」と書いていますが、結果的にはどちらもtrueになります。
しかし、trueになるタイミングが少し感覚と違うので、そこの違いをこの記事でははっきりさせたいと思います。

Unityの非同期ロード系の関数で、
- シーンを非同期でロードする、SceneManager.LoadSceneAsync()
- Resourcesのアセットを非同期で読み込むResources.LoadAsync<T>()
みたいなのがあると思います。(AssetBundleは一旦無視で進行します)

そしてこの2つはともに、UnityEngine.AsyncOperationを返します。
Resources.LoadAsync<T>()ResourceRequestを返しますが、AsyncOperationを継承しているので実質同じ。

しかし、この2つのAsyncOperationAsyncOperation.isDoneをきちんと返してくれるのでしょうか?以前にそこで詰まって頭の中で個チャゴチャになっていたので、もう一度検証してみました。

検証用コード

using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

public class LoadAsyncTest : MonoBehaviour {
    void Start() {
        StartCoroutine(Verify_LoadSceneAsync("scenename"));
        StartCoroutine(Verify_LoadAsync("path"));
    }

    IEnumerator Verify_LoadSceneAsync(string name) {
        AsyncOperation operation = SceneManager.LoadSceneAsync(name);
        operation.allowSceneActivation = false;

        while (!operation.isDone) {
            Debug.Log(operation.progress * 100f + "%読み込み完了");
            yield return null;
        }

        yield return new WaitForSeconds(0.5f);
        operation.allowSceneActivation = true;
    }

    IEnumerator Verify_LoadAsync(string name) {
        ResourceRequest operation = Resources.LoadAsync(name);

        while (!operation.isDone) {
            Debug.Log(operation.progress * 100f + "%読み込み完了");
            yield return null;
        }

        yield return new WaitForSeconds(0.5f);
        Instantiate((GameObject) operation.asset); 
    }
}

LoadLevelAsync()ではシーンを非同期でロードして、whileでisDoneがtrueになるまで待っています。
LoadAssetAsync()ではResourcesのアセットを非同期でロードして、同様の処理を行っています。

検証結果

  ScneneManager.LoadSceneAsync() Resources.LoadAsync()
isDone false true
状態 isDoneが一生trueにならないのでスタック きちんとtrueになり、インスタンス化も成功

そうなんです。LoadSceneAsync()のときのみ、isDoneがtrueにならずスタックします。
同様に、コード内のoperation.progressも0.9fで止まってしまいます。
この場合のisDoneは、シーン遷移が完了してからtrueになるのでロードした段階ではfalseです。

Resources.LoadAsync()は、ロード完了した段階でisDoneがtrueを返してくれるので、普通に使っても問題ないと思います。

検証は以上です。何か記事の中に問題がありましたら、コメントにてご指摘お願いします!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む