- 投稿日:2021-01-18T23:43:36+09:00
超入門!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を持ったインスタンスが出来上がりました。
- 投稿日:2021-01-18T23:12:56+09:00
【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を呼び出すと、小数点以下は切り捨てられますが、これはエラーではありません。)また、型変換はどんな型同士でも可能というわけではありません。
数値同士の型変換など、関連性のある型同士に限りますので注意が必要です。
- 投稿日:2021-01-18T22:20:24+09:00
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になるまで、その条件を毎フレーム評価し続ける。ということは、
All
がtrue
を返すまで、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
)。実は公式ドキュメントの注釈にも書かれている。
- 投稿日:2021-01-18T21:36:11+09:00
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>
- 投稿日:2021-01-18T17:04:33+09:00
プラグインのサンプルを書いて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.今作ると、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 EndGlobalcsproj ファイルを ソリューションファイルに追加する
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で見ると次のイメージクラスローダーの実装
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.GetFullPath
とPath.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\DebugLinux
/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何が違うのか?
パスのデリミタなどはうまく対応できている。自分でパスをプラとフォームに合わせてめんどくさいコードを書く必要はない。ただ、
GetFullPath
とPath.Combine
がある意図がよくわからない。なんでやろ。ドキュメントのサンプルのミス(もともと違うコードで、複数の文字をCombine
してたなど。プラグインの作成
プロジェクトの作成
dotnet new classlib -o HelloPlugin dotnet sln add HelloPlugin/HelloPlugin.csprojcsproj ファイル
Private
は重要で、この設定だと、PluginBase
プロジェクトを参照していますが、この設定が無いと、ここでビルドしたディレクトリ(outputフォルダ)の配下にPluginBase.dll
が作成されます。そうなると、PluginContext
がそのアウトプットディレクトリからアセンブリをロードしてしまいます。ですので、HelloPlugin.HelloCommand
の実装するICommand
はHelloPlugin
の output ディレクトリのICommand
の実装となります。AppWithPlugin
のデフォルトコンテキストからロードされたものになりません。
ExcludeAssets
の設定に関しては、このサンプルに限っては挙動は同じなのですが、プロジェクトが複雑になって、PluginBase
がPackageReferenceを持っている場合、false が参照の方に伝播しないので、そのワークアラウンドとして、設定が必要になります。
- ProjectReference Private
- Plugins should include ExcludeAssets=runtime for project references to shared assemblies
<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
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 なし
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); } }Loading commands from: C:\Users\tsushi\Code\NET\DependencyLoading\HelloPlugin\bin\Debug\net5.0\HelloPlugin.dll -- hello -- Hello !!!
- 投稿日:2021-01-18T00:29:18+09:00
UnityのAsyncOperation.isDoneでtrueになるやつとならないやつ
前提
タイトルで、「trueになるやつとならないやつ」と書いていますが、結果的にはどちらもtrueになります。
しかし、trueになるタイミングが少し感覚と違うので、そこの違いをこの記事でははっきりさせたいと思います。Unityの非同期ロード系の関数で、
- シーンを非同期でロードする、SceneManager.LoadSceneAsync()
- Resourcesのアセットを非同期で読み込むResources.LoadAsync<T>()
みたいなのがあると思います。(AssetBundleは一旦無視で進行します)そしてこの2つはともに、
UnityEngine.AsyncOperation
を返します。
※Resources.LoadAsync<T>()
はResourceRequest
を返しますが、AsyncOperation
を継承しているので実質同じ。しかし、この2つの
AsyncOperation
はAsyncOperation.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を返してくれるので、普通に使っても問題ないと思います。検証は以上です。何か記事の中に問題がありましたら、コメントにてご指摘お願いします!