- 投稿日:2022-02-25T18:01:56+09:00
【ASP.NET】Web未経験者がASPのMSDNチュートリアルやってみる(DB操作)
筆者のステータスはこんな感じ。 VB.NET経験あり C#独学 Web未経験(何それおいしいの?) 時代はWebだということで、手を出してみます。 手を出すチュートリアルはコチラ。 今回はMSDNのチュートリアル「データベースの操作」を進めます。 ※チュートリアルのパート3は説明会なので飛ばしました。 尚、前回実施した「【ASP.NET】Web未経験者がASPのMSDNチュートリアルやってみる(モデルの追加)」の続きから始めます。 #作成の流れ まずは今回の作成の流れを確認します。MSDNのチュートリアルに沿って次の様に進めます。 データベース接続 データベースのシード シード初期化子の追加 実行 今回のチュートリアルも簡単そうに見えますね。 1.データベース接続 まずはデータベース接続の話。 MSDNのチュートリアルでは、RazorPagesMovieContext オブジェクトの説明が記載されています。 RazorPagesMovieContext オブジェクトは、データベースへの接続と、データベース レコードへの Movie オブジェクトのマッピングのタスクを処理します。 データベース コンテキストは、Startup.cs の メソッドで依存性の注入コンテナーに登録されます。 唐突に出て来るRazorPagesMovieContextオブジェクトというのは、DbContextクラスの派生クラスであるRazorPagesMovieContextクラスのことですね。 Startup.csの中を見ると、 public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddDbContext<RazorPagesMovieContext>(options => options.UseSqlServer(Configuration.GetConnectionString("RazorPagesMovieContext"))); } と記載されています。 services.AddDbContext はEntityFrameworkServiceCollectionExtensionsクラスの中に定義されています。 じゃあ、DBに接続する接続文字列はどこに書くのかというと、appsettings.jsonファイルの中で記載します。 { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "ConnectionStrings": { "RazorPagesMovieContext": "Server=(localdb)\\mssqllocaldb;Database=RazorPagesMovieContext-918ff605-1ec6-41e0-92e5-1c1a816338c5;Trusted_Connection=True;MultipleActiveResultSets=true" } } ConnectionStringsの後ろにキーを渡して、接続文字列を記載します。 実際にローカルに作られたDBを見てみます。 Visual Studio のメニューバー「表示」から「SQL Server オブジェクト エクスプローラー」を選択します。 すると、Visual Studio の画面左側に、新しく「SQL Server オブジェクト エクスプローラー」が表示されます。 次に、「(localdb)\MSSQLLocalDB」→「データベース」→「RazorPagesMovieContext-xxxx(キー)」→「テーブル」の順に開き、「dbo.Movie」を右クリックして「デザイナーの表示」を選択します。 すると、テーブルの定義が表示されます。 この定義はMovie.csで記述されているプロパティを元に、EntityFramework が自動で生成したものです。 Movie.csの最初に書かれているプロパティが主キーに設定されます。 namespace RazorPagesMovie.Models { public class Movie { public int ID { get; set; } //EntityFramework が自動で主キーに設定 public string Title { get; set; } [DataType(DataType.Date)]public DateTime ReleaseDate { get; set; } public string Genre { get; set; } public decimal Price { get; set; } } } 次にテーブルのデータを見てみます。 「dbo.Movie」を右クリックして「データの表示」を選択します。 すると、テーブルのデータが表示されます。 チュートリアルで作成したデータはここで見ることが出来ます。 2.データベースのシード 引き続き、MSDNのチュートリアルに沿って進めます。 次に出て来るのは「データベースのシード」について。 いきなり出てきましたシード。シードとは英語で種という意味ですが、ここでは初期のサンプルデータのことを指します。 RazorPagesMovie プロジェクトに「SeedData」フォルダを作成し、その中に「SeedData」クラスを作成します。 「SeedData」フォルダを作成して… 「SeedData」クラスを作成(「SeedData.cs」ファイルが作成されます)。 そして、「SeedData.cs」ファイルに以下を記述。 using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using RazorPagesMovie.Data; using System; using System.Linq; namespace RazorPagesMovie.Models { public static class SeedData { public static void Initialize(IServiceProvider serviceProvider) { using (var context = new RazorPagesMovieContext( serviceProvider.GetRequiredService< DbContextOptions<RazorPagesMovieContext>>())) { if (context.Movie.Any()) { return; // DB has been seeded } context.Movie.AddRange( new Movie { Title = "When Harry Met Sally", ReleaseDate = DateTime.Parse("1989-2-12"), Genre = "Romantic Comedy", Price = 7.99M }, new Movie { Title = "Ghostbusters ", ReleaseDate = DateTime.Parse("1984-3-13"), Genre = "Comedy", Price = 8.99M }, new Movie { Title = "Ghostbusters 2", ReleaseDate = DateTime.Parse("1986-2-23"), Genre = "Comedy", Price = 9.99M }, new Movie { Title = "Rio Bravo", ReleaseDate = DateTime.Parse("1959-4-15"), Genre = "Western", Price = 3.99M } ); context.SaveChanges(); } } } } 「SeedData」クラスの記述を見て見ると、Initialize メソッドでcontext.Movie.AddRangeを実行し、引数には、プロパティを用意している「Movie」クラスのインスタンスを生成し、それぞれのプロパティに値をセットしていることが分かります。 つまり、「SeedData」クラスが使われると、自動的に初期データが用意されるはずです。 ただし、今回追加したクラスのため、この「SeedData」クラスを呼び出す必要があります。 3.シード初期化子の追加 先ほど作成した「SeedData」クラスを呼び出す記述を、「Program」クラスの Main メソッドに追加します。 チュートリアルだと差分が分かりにくいので、コメントアウトで追加・修正個所を記載しておきました。 using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; //追加 using RazorPagesMovie.Models; //追加 namespace RazorPagesMovie { public class Program { public static void Main(string[] args) { //CreateHostBuilder(args).Build().Run(); //元々の記述 var host = CreateHostBuilder(args).Build(); //修正 //追加 start using (var scope = host.Services.CreateScope()) { var services = scope.ServiceProvider; try { SeedData.Initialize(services); } catch (Exception ex) { var logger = services.GetRequiredService<ILogger<Program>>(); logger.LogError(ex, "An error occurred seeding the DB."); } } host.Run(); //追加 end } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); } } Main メソッド内に 先ほど追加した「SeedData」クラスの Initialize メソッドが呼び出されているので、シードの生成が可能となります。 4.実行 では、一度ビルドして実行してみます。 実行して、https://localhost:xxxx/movies に入ってみると… 残念ながら出来てませんね。 完成したからとりあえず実行した人はこうなります(私はなりました)。 さて、MSDNのチュートリアルに戻ると、「データベース内のレコードを削除しなさい」とあります。 なので、ブラウザから「Delete」をクリックして削除しておきます。 せっかく作ったデータが消えてしまいましたね。 ただ、本題はシードデータを追加する事です。 アプリを動かした状態で、システムトレイの「IIS Express」アイコンを右クリックします。 コンテキストメニューから「終了」(もしくは「サイトの停止」)を選択します。 すると、「すべてのワーカープロセスを停止して終了しますか?」とポップアップが出るので、迷わず「はい」を選択。 そしてF5キーを押して、https://localhost:xxxx/movies に入ってみると… シードのデータが作成されていることがわかります。 参考
- 投稿日:2022-02-25T16:52:40+09:00
TeamsのIncomming Webhookにメンション付きメッセージを送信する
この記事について 以前はTeamsのコネクタで「Incomming Webhook」(受信Webフック)を構成してもメンションができなかったのでいまいち使いづらかったのだけど、どうやらメンション付きのメッセージが送信できるようになったらしい、という記事を参考に実際に送信までした手順を記録しておきます。 参考記事 Microsoft TeamsのIncoming Webhookでメンションすると通知が行くようになった Teamsでコネクタを構成する 下記の手順を参考にして、メッセージを送信したいチャネルでコネクタを構成します。 受信 Webhook の作成 作成されたURLを控えておきます。 送信するbodyデータ 次のjsonで送信できます。 Microsoft Teams のカードの書式設定 を内のjsonを送信してみてもうまくいかなかったのだけど、attachmentsリスト内にアダプティブカード書式で追加すればよいことがわかりました。 メンションの指定の仕方がちょっと特殊だな、と思いました。<at>mentioned</at>はこのタグで囲まれていればどんな文字列(プレースホルダーみたいな感じ)でも良さそうです。 { "type": "message", "attachments": [ { "contentType": "application/vnd.microsoft.card.adaptive", "content": { "type": "AdaptiveCard", "version": "1.0", "body": [ { "type": "TextBlock", "text": "<at>mentioned</at>さん、こんにちわ。[今日の天気](https://tenki.jp/)は**晴れ**です。今日の予定は10時から*ミーティング*です。", "wrap": true }, { "type": "TextBlock", "text": "- 箇条書き1\n- 箇条書き2\n- 箇条書き3\n" }, { "type": "TextBlock", "text": "1. 番号付きリスト1\n2. 番号付きリスト2\n3. 番号付きリスト3\n" } ], "actions": [ { "type": "Action.OpenUrl", "url": "https://www.example.com", "title": "リンクを開く" } ], "msteams": { "entities": [ { "type": "mention", "text": "<at>mentioned</at>", "mentioned": { "id": "user@example.com", "name": "ユーザー" } } ] } } } ] } Postmanで動作確認 送信結果 c#でのコーディング c# ではAdaptiveCardオブジェクトが定義されているのでそれを利用して次のようにコーディングできます。 コーディング例 var adaptiveCard = new AdaptiveCard("1.0") ; adaptiveCard.Body.Add( new AdaptiveTextBlock() { Text = $"<at>mentioned</at>さん、こんにちわ。[今日の天気](https://tenki.jp/)は**晴れ**です。今日の予定は10時から*ミーティング*です。", Wrap = true } ); adaptiveCard.Body.Add( new AdaptiveTextBlock() { Text = $"- 箇条書き1\n- 箇条書き2\n- 箇条書き3\n", } ); adaptiveCard.Body.Add( new AdaptiveTextBlock() { Text = $"1. 番号付きリスト1\n2. 番号付きリスト2\n3. 番号付きリスト3\n", Wrap = true } ); adaptiveCard.Body.Add( new AdaptiveTextBlock() { Text = $"<at>mentioned</at>さん、こんにちわ。[今日の天気](https://tenki.jp/)は**晴れ**です。今日の予定は10時から*ミーティング*です。", Wrap = true } ); adaptiveCard.Actions.Add(new AdaptiveOpenUrlAction() { Title = "リンクを開く", Url = "https://www.example.com", }); adaptiveCard.AdditionalProperties.Add("msteams", new { entities = new List<object>() { new { type = "mention", text = @"<at>mentioned</at>", mentioned = new { id = "user@example.com", name = "ユーザー" } } } }); var serializerOptions = new JsonSerializerOptions() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; var webhookMessage = new { Type = "message", Attachments = new List() { new { ContentType = "application/vnd.microsoft.card.adaptive", Content = JsonSerializer.Deserialize(adaptiveCard.ToJson(), typeof(object), serializerOptions), } } }; var webhookUrl = "<チャネルのコネクタで構成したIncomming WebhookのURL>" var client = new RestClient(webhookUrl) { Timeout = -1 }; var request = new RestRequest(Method.POST); request.AddHeader("Content-Type", "application/json"); var body = JsonSerializer.Serialize(webhookMessage, serializerOptions); request.AddParameter("application/json", body, ParameterType.RequestBody); var response = client.Execute(request); Console.WriteLine(response.Content);//成功した場合はContentに"1"が返る。
- 投稿日:2022-02-25T16:51:23+09:00
C#コードの最適化:Monoメモリの節約
C#は仮想マシン(VM)環境(Mono仮想マシンまたはIL2CPP仮想マシン)で実行され、割り当てられたMonoメモリは仮想マシンによって管理および再利用されるため、C#コードは「マネージドコード」と呼ばれ、Monoメモリも「マネージドメモリ」として知られています。「マネージドメモリ」の解放は、仮想マシンのGC(ガベージコレクション)メカニズムに依存しますが、この記事では説明していません。 C#プログラム開発で従うべき基本原則は、不要なMonoメモリの割り当てを回避することであり、Monoメモリの割り当ては主に次の結果を引き起こします。 プログラムが使用するメモリの総量が多すぎます。メモリは限られたアセットです。ゲーム(特にモバイルゲーム)の場合、メモリ使用量は貴重なものであり、解放できないメモリが多すぎると、プログラムがクラッシュする可能性があります。 割り当ての回数が多すぎると、Monoの断片化が増加し、必要な連続メモリを割り当てることができなくなり、プログラムがクラッシュする可能性があります。 メモリ割り当てはGC(ガベージコレクション)をトリガーし、GCは高価であり、遅延を引き起こします。 ##1.当クラスのメソッドには.tagへのコールがある tagはシーン内のGameObjectのタグであり、クラスGameObjectのメンバーtagは属性です。属性を取得するとき、基本的にget_tag()関数をコールし、nativeレイヤーから文字列を返します。文字列は参照型であり、この文字列が返されると、Monoメモリが割り当てられます。ただし、Unityエンジンはキャッシュによってget_tagを最適化しません。get_tagがコールされるたびに、Monoメモリが再び割り当てされます。 したがって、tagを比較する必要がある場合は、関数GameObject.CompareTag()を使用することをお勧めします。この関数は、nativeレイヤーに実装され、マネージMonoメモリの割り当てを引き起こさないため、GCの負荷を回避できます。 ##⒉当クラスのメソッドには、texture GetPixels()/ GetPixels32()のコールがある Texture2DタイプのオブジェクトでGetPixels()およびGetPixels32()をコールすることは、通常、指定されたMipmapレイヤーのすべてのピクセル情報を取得することであり、画像上のピクセル数は非常に多いことがよくあります。 メモリ割り当てに関しては、この関数はマネージMonoにメモリを割り当てて、テクスチャデータのピクセル情報を格納しますが、エンジンはそれをキャッシュしません。したがって、頻繁にコールされる関数で使用すると、永続的なMonoメモリ割り当てが発生します。 時間の面では、大規模な並列演算が得意なGPUが画像情報を処理するのは非常に簡単ですが、ピクセルごとの情報を取得する場合、CPUは少し面倒です。また、GetPixels()はCPUによって同期的に実装されるため、時間がかかり、コールのスレッドが同時にブロックされ、ラグが発生する可能性があります。したがって、必要な場合を除いて、GetPixels()を使用することはお勧めしません。 ##3.当クラスのメソッドにGetComponentsInChildrenコールがある/当クラスのメソッドにGetComponentsInParentコールがある 前回の記事「C#コードの最適化:CPU時間の節約」では、GetComponentsInChildrenとGetComponentsInParentについて簡単に説明しました。ここでは、実際のコールでは、オブジェクトのトラバーサルと結果の戻りが含まれるため、不適切に使用すると、永続的なMonoメモリの割り当てが発生することをさらに説明します。開発チームは、List型の参照をパラメータとして受け入れるバージョンを使用することをお勧めします。これにより、コールごとにMonoメモリを割り当てないようにすることができます。 ##4.当クラスのメソッドにLinq関連関数へのコールがある Linq関連の関数は、通常、データのクエリと処理に使用されます。機能的に言えば、一連のデータに対してさまざまなif判定とforループ処理を実行することです。 Linqが提供するAPIを使用すると、集合体データを処理するSQLステートメントスタイルのコードを記述できます。これにより、コードの単純さと読みやすさが大幅に向上し、保守も便利になり、書き込み効率が向上しますが、これらの利点はパフォーマンスが犠牲になります。 Linqは実行中にいくつかの一時変数を生成し、デリゲート(lambda式)を使用します。デリゲートを条件付き判定方法として使用すると、時間のコストが高くなり、一定量のMonoメモリが割り当てられます。したがって、一般的なUnityゲームプロジェクトの開発では、Linq関連の関数を使用することはお勧めしません。エディター機能の開発では、LinqとReflectionを一緒に使用することがよくあります。 ##5.当クラスのメソッドに、RendererにsharedMaterialsの取得がある 同様に、前回の記事「C#コードの最適化:CPU時間の節約」では、.material / materialsについて説明しました。簡単に言うと、.material(s)をコールすると、新しいシェーダーインスタンスが生成されます。sharedMaterialsは共有マテリアルであり、新しいマテリアルインスタンスは生成されません。ただし、.sharedMaterialsをコールすると、Monoメモリが割り当てられます。コールごとに、Materialのインデックスを保持する配列が割り当てられます。このアレイのメモリ使用量は比較的小さいですが、頻繁にコールすることはお勧めしません。 ##6.当クラスのメソッドにInput.touchesコールがある モバイルプロジェクトの相互作用では、タッチ操作は非常に頻繁で一般的であると言えます。タッチ操作の取得では、Input.touchesを使用して、現在のフレーム内のすべてのタッチ操作のステータスと対応するデータを取得します。しかし、.touchesの実装を見ると、コールたびにtouchesの配列がnewになり、ある程度のMonoメモリが割り当てられることがわかります。したがって、開発チームは、追加のMonoメモリの使用を防ぐために、Input.touchesを頻繁に使用しないようにする必要があります。 ##7.FindObjectsOfTypeコール 前回の記事「C#コードの最適化:CPU時間の節約」では、FindObjectsOfTypeについて簡単に紹介しました。 CPU時間は増加する上に、Monoメモリ割り当てのかなりの部分を占めるため、パフォーマンスへの影響を軽減するために、1回のコールで結果をキャッシュすることをお勧めします。 ##8.当クラスのメソッドにTextAsset / WWW.bytesコールがある このルールは、実際には2つの無関係なUnityAPI用です。 1つ目は、TextAssetのbytesプロパティです。 TextAssetはUnityのテキストアセットであり、txt、html、bytes、csvなどのさまざまな形式のファイルの変換をサポートしています。 bytesプロパティを取得するとき、Unityはnativeレイヤーからバイト配列(byte [])を取得し、それによって一定量のMonoメモリを割り当てます。 別のAPIは、WWWクラスのbytesメンバーを参照します。呼び出されるたびに、Monoメモリの割り当てが発生します。UnityがWWWクラスの関連するインターフェイスを放棄したため、UnityWebRequestを使用して関連機能を実現することをお勧めします。 UWA Technologyは、モバイル/VRなど様々なゲーム開発者向け、パフォーマンス分析と最適化ソリューション及びコンサルティングサービスを提供している会社でございます。 今なら、UWA GOTローカルツールが15日間に無償試用できます!! よければ、ぜひ! UWA公式サイト:https://jp.uwa4d.com UWA GOT OnlineレポートDemo:https://jp.uwa4d.com/u/got/demo.html UWA公式ブログ:https://blog.jp.uwa4d.com
- 投稿日:2022-02-25T16:24:19+09:00
Serilog 5.0.0 バージョンアップ時に引っかかった2点
はじめに Serilog をバージョンアップ(v4.1.0 ⇒ v5.0.0)したら警告と実行時エラーに遭遇したので、対処方法をメモしておきます。 IWebHostBuilder に対する AddSerilog が非推奨になった IWebHostBuilder は ASP.NET Core 2.1 でアプリケーションの初期化に利用されていたインターフェイスで、ASP.NET Core 3.1 以降では IHostBuilder に置き換えることが推奨されています。 Serilog 5.0.0 では、IWebHostBuilder に対する AddSerilog メソッドが非推奨としてマークされているので、ASP.NET 2.1 から 3.1 への移行時にIWebHostBuilder のまま移行していた場合、Serilog を 5.0.0 以上にバージョンアップすると下記の警告が表示されます。 警告 CS0618 'SerilogWebHostBuilderExtensions.UseSerilog(IWebHostBuilder, Action<WebHostBuilderContext, LoggerConfiguration>, bool, bool)' は旧形式です ('Prefer UseSerilog() on IHostBuilder') 例えば次のような ASP.NET Core 5 までの初期化コードの場合、 Program.cs public class Program { public static void Main(string[] args) { Log.Logger = new LoggerConfiguration() .WriteTo.Console() .CreateBootstrapLogger(); try { CreateHostBuilder(args).Build().Run(); } catch (Exception ex) { Log.Fatal(ex, "Unhandled exception"); } finally { Log.Information("Shut down complete"); Log.CloseAndFlush(); } } public static IWebHostBuilder CreateHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseSerilog((context, configuration) => configuration.ReadFrom.Configuration(context.Configuration)); .UseStartup<Startup>(); } 下記のような IHostBuilder を使った形に書き換えてあげればよいですね。 Program.cs public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseSerilog((context, configuration) => configuration.ReadFrom.Configuration(context.Configuration)); .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); もしくは、.NET 6.0 の Mimimal API の形に書き換えてもよいです。 Program.cs Log.Logger = new LoggerConfiguration() .WriteTo.Console() .CreateBootstrapLogger(); try { var builder = WebApplication.CreateBuilder(args); builder.Host.UseSerilog((context, configuration) => configuration.ReadFrom.Configuration(context.Configuration)); // ... 略 ... var app = builder.Build(); // ... 略 ... app.Run(); } catch (Exception ex) { Log.Fatal(ex, "Unhandled exception"); } finally { Log.Information("Shut down complete"); Log.CloseAndFlush(); } IWebHostBuilder を自前で BuildServiceProvider すると例外が発生する 以前から Startup 時に自前で IServiceCollection をビルドすると、シングルトンが複数生成されるなどしておかしくなるので非推奨でしたが、Serilog 5.0.0 以上を利用している場合、ServiceCollection をビルドしたタイミングで実行時エラーが発生します。 System.InvalidOperationException: The logger is already frozen. at Serilog.Extensions.Hosting.ReloadableLogger.Freeze() at Serilog.SerilogHostBuilderExtensions.<>c__DisplayClass3_1.<UseSerilog>b__1(IServiceProvider services) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument) ... 略 ... at Microsoft.Extensions.Hosting.HostBuilder.Build() at Microsoft.AspNetCore.Builder.WebApplicationBuilder.Build() at Program.<Main>$(String[] args) in C:\Users\YoichiSugiyama\source\repos\WebApplication1\WebApplication2\Program.cs:line 18 初期化中に DI コンテナの中身が欲しくなる多くのケースは Scope か Singleton なサービスの初期化だと思うので、次のように書き換えます。 例えば、DbContext に独自の LoggerFactory を渡したい場合は、サービスコンテナをビルドしてサービスを取り出すのではなく、初期化時にサービスコンテナを受け取りサービスを取り出します。 変更前 var factory = builder.Services.BuildServiceProvider().GetRequiredService<ILoggerFactory>(); builder.Services.AddDbContext<MyDbContext>(options => { options.UseLoggerFactory(factory); options.UseMySql(builder.Configuration.GetConnectionString("myDb"), ServerVersion.Parse("5.7")); }); 変更後 builder.Services.AddDbContext<MyDbContext>((services, options) => { options.UseLoggerFactory(services.GetRequiredService<ILoggerFactory>()); options.UseMySql(builder.Configuration.GetConnectionString("myDb"), ServerVersion.Parse("5.7")); }); シングルトンなサービスの初期化も同様ですね。 変更前 builder.Services.AddSingleton<DbSettingValueProxy>(); var proxy = builder.Services.BuildServiceProvider().GetRequiredService<DbSettingValueProxy>(); builder.Services.AddSingleton(new HogeService(new HogeServiceOptions { Option1 = proxy.GetValue("prop1") })); 変更後 builder.Services.AddSingleton<DbSettingValueProxy>(); builder.Services.AddSingleton(services => { var proxy = services.GetRequiredService<DbSettingValueProxy>(); return new HogeService(new HogeServiceOptions { Option1 = proxy.GetValue("prop1") }); }); サービス側が Options パターンを使っている場合は、Configure 時に型パラメーターに取得用のサービスを指定すれば DI で解決することができます。 変更前 builder.Services .AddOptions<HogeServiceOptions>() .Configure(options => { var proxy = builder.Services.BuildServiceProvider().GetRequiredService<DbSettingValueProxy>(); options.Option1 = proxy.GetValue("prop1"); }); 変更後 builder.Services .AddOptions<HogeServiceOptions>() .Configure<DbSettingValueProxy>((options, proxy) => { options.Option1 = proxy.GetValue("prop1"); }); まとめ 行儀が悪い事をしていると、ある日困ったことになります。
- 投稿日:2022-02-25T14:26:46+09:00
猫でも分かる!Unityでコライダーの大きさをオブジェクトのscale値の影響を受けないようにする方法!!?
前口上 unityでゲームを作っていて、演出としてゲーム上のオブジェクトのscale値をアニメーションとかで変えることってよくありますよね!(あるよね?) でも、コライダーがついていると、そのオブジェクトのscale値の変動にあわせて、しっかりコライダーの大きさも変わってくれますよね~ すごく便利でありがたい機能ではあるんですけど、時としてこの機能が邪魔になってしまう事もゲームを作っていれば1回ぐらいはあるはず... 今回はそういう人に向けて、今回私がこの問題に当たった時に解決した方法を猫でも分かるように書いていきたいと思います! こんな挙動に困ってました? この動画では、下の球をアニメーションでscale値を変更しているのですが、コライダーも一緒に変っているため、上に乗っかった球が一緒に動いているのが分かると思います 今回作っているゲームでは、コライダーの接触時に反射判定をしているため、何度もコライダーに当たるような挙動になりそうで困っていました... 試したこと① 真っ先に試したこととして、まずsphrerColliderのリファレンスを確認しました 困ったときは解説記事を読む前に公式のリファレンスを読んで自分で理解する力を身に着けることも大事です!! 今回困っているのはcolliderの大きさなので、radiusの項目を読んでみると、 「ローカルサイズでのコライダーの半径」とありますね つまり、radiusの値はどれだけ弄ってもローカルの値なので、そのオブジェクトのscale値によって変わってしまうのです? もう少しわかりやすく解説すると、コライダーの大きさは、scaleのxyzいずれかのうち最大の値 * radius でコライダーの半径が求められます scale : x=2, y=1, z=1 radius: 0.5 の場合 transform.scale.x * radius = 1 = 2 * 0.5 = 1 となりますね! そして、答えの「1」という値は半径なので、直径に直すと「2」となり、「scale.x」の値と同じになりますよね だからsphireColliderのradiusはデフォルトで「0.5」になっているんですね~ 私もこうやって改めて調べてみて初めて知ることができました? ①の結果を受けて... はてさて知識を蓄えられたのはいいものの、肝心の問題は解決できませんでした これでは、結局いくらinspector上でradiusの値を変えようが意味がないという事しかわかっていないですね? しかし逆に言えば、それ以外のアプローチを試すきっかけが出来たので、さっそく他の方法を考えてみました 先にinspector上での値操作はダメと書いたので、分かった方もいるかもしれませんが、動的に変化させるというのが正解な気がしますね! 私もそう思い、動的にradiusの値を変更する方法を2つ考えました 1つめは、アニメーションで変更する方法です 私自身がアニメーションを得意とするのもあり、真っ先に思い浮かびました 計算式はもう出ているので、それに当てはめてscale値が変わるのに合わせてradiusの値も変化させればうまくいくだろうと思いました 2つめは、スクリプトから変更する方法です 別に避けてたわけではないですが、なぜか最終手段だと思って最後まで考えないようにしていました スクリプトを書けば解決するのは分かっていたんですけどね... 試したこと② 先に挙げたように、アニメーションを使えばできるだろうと私はかなり自信を持っていました しかし、結果から言えば、この方法ではできなくもないが、かなり厳しいです? アニメーションはオブジェクトのinspectorにある情報のほとんどを動的に変化させることが可能です 実際どのようにradiusの値を変化させようかと思ったときに、先の計算式を使うとすごく簡単です scale * raduis = コライダーの半径 初期サイズがscale(1,1,1)の球で、コライダーの半径は常に初期サイズの半径と同じであってほしいので、「0.5」とします 求める値はradiusです 式変換をし、 radius = コライダーの半径 / scale で求められますね ということで、 こんな感じでAnimationに入力していきます ちなみに、unityエディター上の数値入力のフィールドでは四則演算、累乗、かっこを用いた計算ならフィールド内で出来ますよ! さて、この計算式を使って、すべてのscaleアニメーションのピボットがある位置にradiusの値を入力していきました 各ピボットを見てみるとscaleが変わってもコライダーのサイズは変わってないです! これは行った!!と思いました しかし、実際に動かしてみるとダメでした なぜこうなったのか 原因を色々と考えたのですが、どう見てもピボットが打たれている場所以外の線形補完された部分で大きさに差異が出ていることには気付きました しかし、なぜそうなるのかよく分からなくて明確な答えにはたどり着けていないので、この先は考察も含めた内容です unityのアニメーションでは、ピボット間の数値の変化は直線的な補完ではなく、サインカーブのような補完がされます サインカーブのような補完というのは、最初は数値の変動が少なく、中央に近づくにつれて変動が大きくなり、終わりにつれてまた変動が少なくなるということです(語彙力) これは、アニメーションウィンドウで表示の仕方をカーブに変えるとわかると思います 分かりやすいように、それぞれの値を大きく、ピボット間の時間を長くしてあります これはピボットがある位置ではコライダーの大きさが変わっていないグラフです このように、各値がぐにゃ~ってなっているんですよね おそらくこれが原因なのは確定なのですが、どうしてこれがダメなのかはよく分かりません(本末転倒) 教えて!つよつよプログラマー! 最終的な解決方法 結局、スクリプトを書いてやるしかないみたいです? ピボットを打った位置なら正常に動くのであれば、スクリプト上から毎フレームピボットでやったことと同じことをしてあげればいいだけの話です いたって簡単なのに、なぜ最後までやらなかったのか... colliderScalling.cs using System.Collections; using System.Collections.Generic; using UnityEngine; public class colliderScalling : MonoBehaviour { [Header("このオブジェクトのSphereCollider"),SerializeField] SphereCollider col; [Header("絶対的なコライダーのサイズ(半径)"), SerializeField,Space(10)] float radiusAbsoluteSize; private void Start() { col = GetComponent<SphereCollider>(); } private void Update() { //絶対サイズ / scale.x,y,zの最大値 col.radius = radiusAbsoluteSize / Mathf.Max(this.transform.localScale.x,this.transform.localScale.y,this.transform.localScale.z); } } たったこれだけで実装できてしまいます やっている事もさっきまでやってきた事をそのままスクリプトで毎フレーム計算しただけです なんならこっちの方が球のサイズに左右されずに作れるので拡張性が高いですね! まとめ? いかがだったでしょうか? 私が普段問題解決をする際の流れも込みでいろいろ書いてみました あえてスクリプトを書こうとしなかったことで学んだことがあったので、自分的にはよかったかなって思います! こういう普段何となく使っているツールの機能も、意外と調べたり、触ってみたりすると面白い動きをしていたり、逆に不便だと思ったりすることはよくあります 特に、Unityはかなり良くできたゲームエンジンですが、それ故に頼りすぎてしまう部分多いと思うので、ただ作るだけではなく、もう少し踏み込んでみるとより面白くなるのでお勧めですよ!!
- 投稿日:2022-02-25T12:53:39+09:00
PostgreSQL EF CoreでのJsonの処理
EntityFrameworkCore の7.0プレビューが出たので、PostgreSQL EF Coreでの、Jsonの処理について検証しました。 環境 Microsoft.EntityFrameworkCore 7.0.0-preview.1.22076.6 Npgsql 7.0.0-preview.1 Npgsql.EntityFrameworkCore.PostgreSQL 7.0.0-preview.1 .NET 6 Npgsql.Json.NET 7.0.0-preview.1 は必要なさそうです。 前提 PostgresSQLの、Entiry Framework CoreでのJSONの取り扱いは、 A) String mapping B) POCO mapping C) JsonDocument DOM mapping の3種類 Aの文字列で処理するのは、自分でJSONへのシリアル化と逆シリアル化(パース)をする。他の2つに比べて、制限された方法である、と解説されています。 メリット・デメリット 以下、AとBとを実際に実装してみて得られた知見。 Aの方法 自分でシリアル化・逆シリアル化が必要。 タイミングとして、EFで、ContextをLoadする時に逆シリアル化し、Saveする前にシリアル化する処理、でOK。(他の処理のタイミングとして、プロパティをgetした時、setした時でも、出来そうだ) このままで context.SaveChangesが機能する。 double.NaN, double.PositiveInfinity, double.NegativeInfinity は、 System.Text.Json(Microsoft)を利用してJsonとして処理できる。 しかし、Brushなど、System.Text.Jsonでも処理できない型があるので、それは文字列にするなどの処理が必要。 Bの方法 自分でJsonのシリアライズ・逆シリアル化は不要。 ただし、プロパティの値を変更しても、このままではcontext.SaveChangesが機能しない。なんらかの処理が必要。 例1 Jsonのプロパティを変更したと示す、IsModified をtrueにする。 context.Entry(groupB).Property(e => e.CustomerJsonb).IsModified = true; 例2 Change Tracker を機能させるために、ValueComparerを利用する。 dobule.NaNなどは、このままでは処理できない(現時点では)。文字列として処理すればOK。 Jsonで処理出来ない型 AもBもJsonとして処理できない型は、文字列にしたりして保存すると良い。 例:Brush doubleの無限大・非数をJson用に文字列にする例 Bの方法で利用します。Aの方法で、System.Text.Json(Microsoft)を利用するなら、無限大・非数をJsonにできるので、必要ないです。 テストした限りではこれで機能すると思います。 アプリ内部では、Numberを利用。Jsonには、NumberSを利用。 (string?とかの?は、C# 8.0以降の、Null 許容参照型を示します) private double _number; [JsonIgnore] public double Number { get { return _number; } set { _number = value; _numberS = value.ToString(); } } private string? _numberS; public string? NumberS { get { return _numberS; } set { _numberS = value; _number = double.Parse(value); } } BrushをJson用に文字列にする例 アプリ内部では、Colorプロパティを利用。Jsonには、ColorSを利用。 SolidColorBrushのみ対象にしています。 private Brush? _color; [JsonIgnore] public Brush? Color { get { return _color; } set { _color = value; _colorS = value?.ToString(); // → value is null ? null : value.ToString(); } } private string? _colorS; public string? ColorS { get { return _colorS; } set { _colorS = value; _color = Utils.GetBrushFromString(value); } } Bの方法でデーターベースに保存されたJsonの例 PostgreSQLの、jsonb と、json とは少し違います。ただ、 「\u221E」 は、「∞」 なので、この点は単に表示の違いだと思います。 Bの方法のEntity全体 public class GroupB { public int Id { get; set; } [Column(TypeName = "jsonb")] public CustomerB? CustomerJsonb { get; set; } //Jsonbで保存 [Column(TypeName = "json")] public CustomerB? CustomerJson { get; set; } //Jsonで保存 } public class CustomerB { public string? Name { get; set; } private double _number; [JsonIgnore] public double Number { get { return _number; } set { _number = value; _numberS = value.ToString(); } } private string? _numberS; public string? NumberS { get { return _numberS; } set { _numberS = value; _number = double.Parse(value); } } private Brush? _color; [JsonIgnore] public Brush? Color { get { return _color; } set { _color = value; _colorS = value?.ToString(); // → value is null ? null : value.ToString(); } } private string? _colorS; public string? ColorS { get { return _colorS; } set { _colorS = value; _color = Utils.GetBrushFromString(value); } } } public static class Utils { public static SolidColorBrush? GetBrushFromString(string? brushString) { return string.IsNullOrEmpty(brushString) ? null : (SolidColorBrush?)new BrushConverter().ConvertFromString(brushString); } } 備考 今まで設定の保存に、XMLを利用していたのですが、Jsonの方が扱いやすいです。 double、BrushのJsonシリアル化・逆シリアル化を、改めて考えてみて整理しました。今まで、煩雑な処理をしていたのを、改善できました。 こうして記事にまとめておかないと、自分でも忘れてしまう。 参考 Entity framework not detecting jsonb properties changes in c# How to store JSON in an entity field with EF Core? PostgreSQLにおけるjson/jsonb型の取り扱い EF7でのJSONサポートについて How to serialize a Windows.Media.Brush
- 投稿日:2022-02-25T01:11:28+09:00
.NET nanoFrameworkでマイコンプログラミング (IoTHubに接続)
.NET nanoFrameworkのサンプルプログラム4つ目です。 今回は Azure IoT Hub に接続してみます。 nanoFramework用のDeviceClient(Azure IoT Hub SDK)パッケージがあるので接続は簡単です。 ハードウェア ・M5 ATOM Matrix ・ESP32 DevkitC ソフトウェア ・VisualStudio2019 ・ファームウェア ESP32_PICO 1.7.4-preview.54 ・ファームウェア ESP32_REV0 1.7.4-preview.54 ・Azure IoT Hub 追加するNuGetパッケージ ・nanoFramework.System.Device.WiFi 1.4.0-preview.36 ・nanoFramework.Azure.Devices.Client 1.0.0-preview.222 プログラム Program.cs using System; using System.Diagnostics; using System.Threading; //追加 using System.Device.WiFi; using nanoFramework.Networking; using nanoFramework.Azure.Devices.Client; namespace NF_ESP32_IotHub { public class Program { const string ssid = "YOUR SSID"; const string password = "PASSWORD"; const string deviceId = "DEVICEID"; const string saskey = "SASKEY"; const string iothubAddress = "YOUR IOTHUB ADDRESS"; public static void Main() { if (!ConnectWifi()) { Debug.WriteLine("Connection fail..."); return; } else { Debug.WriteLine("Connected..."); } //クライアント DeviceClient client = new DeviceClient(iothubAddress, deviceId, saskey); var isOpen=client.Open(); Debug.WriteLine($"Connection is open: {isOpen}"); for (int i = 0; i < 10; i++) { client.SendMessage("Hello IoTHub!"); Debug.WriteLine("Send Message..."); Thread.Sleep(10000); } Thread.Sleep(Timeout.Infinite); } private static bool ConnectWifi() { Debug.WriteLine("Connecting WiFi"); var success = WiFiNetworkHelper.ConnectDhcp(ssid, password, reconnectionKind: WiFiReconnectionKind.Automatic, requiresDateTime: true, token: new CancellationTokenSource(60000).Token); if (!success) { Debug.WriteLine($"Can't connect to the network, error: {WiFiNetworkHelper.Status}"); if (WiFiNetworkHelper.HelperException != null) { Debug.WriteLine($"ex: {WiFiNetworkHelper.HelperException}"); } } Debug.WriteLine($"Date and time is now {DateTime.UtcNow}"); return success; } } } ・DEVICEIDはIoT Hubに登録したDevice名になります。 ・SASKEYは登録したデバイスの主キーになります。 ・IOTHUB ADDRESSはIoTHubのホスト名になります。 ・IoTHubに接続するためにはルートCA証明書が必要になります。 証明書の登録方法は 1.ソースコードに埋め込む 2.リソースとしてプロジェクトに加える 3.デバイスにアップロードする 今回は3のデバイスにアップロードするを行います。 ・まず次のページから「Baltimore CyberTrust Root」をダウンロードしておきます。 ・次にVisualStudioをDeviceExplolerウインドウに切り替えて、上部のEdit Network Configurationボタンをクリックします。 ・Generalタブに切り替えて、Browseボタンをクリックします。 ・ファイルダイアログで先ほどダウンロードした証明書ファイルを選択して開くボタンをクリックすると、デバイスに証明書がアップロードされます。 ・プログラムを実行するとIoTHubへメッセージが10秒ごとに10件送信されます。 以上 IoT Hub 接続サンプルになります。 ソースコード一式はこちらからダウンロードできます。
- 投稿日:2022-02-25T00:05:00+09:00
クラス変数の持ち主
これは何? クラス変数の持ち主が、 python と ruby で違うことに気がついたので、他の言語どうなってるんだろと思って調査したのでその記録。ジェネリクスとテンプレートを交えながら。 ruby の場合 ruby class B def set_v(x); @@v=x; end def v(); @@v; end end class C < B;end class D < B;end c = C.new.set_v("C#set_v") d = D.new.set_v("D#set_v") p [ C.new.v, D.new.v ] #=> ["D#set_v", "D#set_v"] クラス変数 @@v の持ち主は、 class B 。 C 経由で変更しても、 D から見た @@v の値が変わる。 Python3 の場合 python3 class B: v = "in-class-def" @classmethod def setV(cls,x): cls.v = x class C(B):... class D(B):... print(B().v, C().v, D().v) #=> in-class-def in-class-def in-class-def C().setV("C.setV") D().setV("D.setV") print(B().v, C().v, D().v) #=> in-class-def C.setV D.setV Python のクラス変数はcls.v のようにするので、v の持ち主は cls で指定される型。なので、クラス変数を参照しているメソッドが書かれているクラスとは関係がない。 最初の print 内にある C.v なんかは、C の基底クラスを見に行っている。 わりと珍しい作戦だと思う。 この作戦だと、深い継承ツリーで偶然同じ名前を使ってしまったために大惨事、みたいなことが起きると思う。 ※ 初出時間違ったことを書いてましたすいません。 Java の場合 Generics なしで まずは Generics なしで。 java import java.io.*; class B{ public static String s; } class C1 extends B {}; class C2 extends B {}; class Foo { public static void main (String[] args) throws java.lang.Exception { C1.s = "c1"; C2.s = "c2"; System.out.printf( "C1.s=%s C2.s=%s\n", C1.s, C2.s ); //=> C1.s=c2 C2.s=c2 } } ここは ruby と同じく、クラス変数の持ち主は、その変数が定義されたクラス。 Generics を使うと Generics を使って、型引数を変えてみると java import java.io.*; class B<T>{ public static String s; T dummy; } class C1 extends B<Integer> {}; class C2 extends B<Long> {}; class Foo { public static void main (String[] args) throws java.lang.Exception { C1.s = "c1"; C2.s = "c2"; System.out.printf( "C1.s=%s C2.s=%s\n", C1.s, C2.s ); //=> C1.s=c2 C2.s=c2 } } 異なる型引数でもクラス変数は同じだということがわかる。 なので、型引数の型を static 変数には使えない。なるほどこれが型消去かという感じ。 C++ の場合 テンプレートなし まずはテンプレートなしで。 c++17 #include <iostream> #include <string> struct B{ static inline std::string s; }; struct C1 : public B{}; struct C2 : public B{}; int main(){ C1{}.s = "c1"; C2{}.s = "c2"; std::cout << C1{}.s << " " << C2{}.s << std::endl; //=> c2 c2 return 0; } ruby, Java と同様、static変数(クラス変数) の持ち主は、その変数が定義されたクラス。 テンプレートあり テンプレートクラスの場合。 c++17 #include <iostream> #include <string> template<int n> struct B{ static inline std::string s; }; struct C1 : public B<0>{}; struct C2 : public B<1>{}; int main(){ C1{}.s = "c1"; C2{}.s = "c2"; std::cout << C1{}.s << " " << C2{}.s << std::endl; //=> c1 c2 return 0; } テンプレート引数が異なるクラスは別クラスになる。なので、Java と違って static 変数の型にテンプレート引数が使える。 C# の場合 Generics なし C# using System; class B{ public static String s; } class C1 : B{}; class C2 : B{}; public class Test { public static void Main() { C1.s = "c1"; C2.s = "c2"; Console.WriteLine( "C1.s={0} C2.s={1}", C1.s, C2.s); //=> C1.s=c2 C2.s=c2 } } まあそうだよね。Java と同じ。 Generics あり Generics ありだと C# using System; class B<T>{ public static String s;} class C1 : B<int>{}; class C2 : B<long>{}; public class Test { public static void Main() { C1.s = "c1"; C2.s = "c2"; Console.WriteLine( "C1.s={0} C2.s={1}", C1.s, C2.s); //=> C1.s=c1 C2.s=c2 } } Java とは異なる。 まとめ ひとくちに「クラス変数」というけれど、言語によって意味が違うので要注意。 Python はたぶん異端で、明示されているレシーバがそのクラス変数の持ち主。基底クラス内の cls.class_var は、 cls が異なれば異なる変数。cls 指すクラスの別のスーパークラスで同名のクラス変数を使うと同じ変数となる。 ruby, Java, C++, C# は、クラス変数が定義されたクラスがクラス変数の持ち主。 Generics / template を使う場合。 型引数が違っても同じクラス、というのが Java の立場。 template 引数 / 型引数が違うクラスは別のクラス、というのが C++ と C# の立場。 他に試すべき言語あるかなぁ。 余談 今は亡き J# でジェネリクス使ったらどうなるんだろ。 そもそも J# でジェネリクス使えるかどうかも知らないけど。 以下、投稿の翌日に追加。 Python と ruby のクラス変数の気持ち悪いところ コメントいただいたり他に試したりして、何が気持ち悪いのかわかってきた。 Python の場合 シンプルな例 まずは普通っぽい例。 Python3 class B:v="B0" class C(B):... class D(B):... # [1] print(B.v, C.v, D.v) #=> "B0 B0 B0" # [2] C.v = "C0" print(B.v, C.v, D.v) #=> "B0 C0 B0" [1] の print 内の C.v は、 B の v のことで、 [2] の C.v="C0" の C.v は、 C の v のことなので、C.v="C0" は D.v で得られる値には影響を与えない。 同じ C.v という式だけど、参照するときと代入の左辺になるときで意味が違うのがわかりにくい。とはいえ、オブジェクト指向とはそのようなものだとも思う。 わかりにくい現象 このわかりにくさと Python の += なんかのわかりにくさが合体して、こんなわかりにくいことが起こる。 Python3 class B: a=["a"] b=["b"] c = "c" d = "d" class C(B):... print(repr([B.a, B.b, B.c, B.d])) #=> [['a'], ['b'], 'c', 'd'] C.a += ["+="] C.b = C.b + ["+"] C.c += "+=" C.d = C.d + "+" print(repr([B.a, B.b, B.c, B.d])) #> [['a', '+='], ['b'], 'c', 'd'] リストの += は新たなオブジェクトは作られないので基底クラスの B.a が書き換わり。 文字列の += は新たなオブジェクトが作られるので代入と同じことになるから B.c は書き換わらない。 かなり気持ち悪い動作だと思うけど、なんでそうなるのかは説明できる感じ。 ruby の場合 シンプルな例 一方 ruby の普通っぽい例を実行すると。 ruby class B def set_vb(x); @@v=x; end def vb(); @@v; end end class C < B def set_vc(x); @@v=x; end def vc(); @@v; end end class D < B def vd(); @@v; end end B.new.set_vb("B0") p [B.new.vb, C.new.vc, D.new.vd] #=> ["B0", "B0", "B0"] C.new.set_vc("C0") p [B.new.vb, C.new.vc, D.new.vd] #=> ["C0", "C0", "C0"] 上記の通り Python とは違う。 class C 内の @@v を書き換えると class B 内で定義された @@v が書き換わる。なので、C の派生クラスでも基底スラスでもない D から見える @@v にも影響を与える。 ここだけ見るとそれはそれでありかなと思うけれど。 わかりにくい現象 ならば、派生で @@v を作ってから基底で @@v に代入すると ruby class B def set_vb(x); @@v=x; end def vb(); (defined? @@v) ? @@v : :undefined; end end class C < B def set_vc(x); @@v=x; end def vc(); (defined? @@v) ? @@v : :undefined; end end # [1] C.new.set_vc("C0") p [B.new.vb, C.new.vc] #=> [:undefined, "C0"] # [2] B.new.set_vb("B0") p [B.new.vb, C.new.vc] # ruby 2.7.5 では、["B0", "B0"] # ruby 3.1.0 では例外。class variable @@v of C is overtaken by B なんと、 ruby 3.1 では例外。 2.7.5 では、基底クラスの @@v を書き換えると派生クラスの @@v が書き換わる。難しい。難しすぎるので例外にしたんだろうと思う。 あるいは。 以下を実行すると。 ruby module B @@x = :bx def self.b() [ (defined? @@x) ? @@x : :undef, (defined? @@y) ? @@y : :undef, (defined? @@z) ? @@z : :undef] end end module C @@y = :cy def self.c() [ (defined? @@x) ? @@x : :undef, (defined? @@y) ? @@y : :undef, (defined? @@z) ? @@z : :undef] end end class D include B, C @@x = :dx @@y = :dy @@z = :dz def self.d(); [ (defined? @@x) ? @@x : :undef, (defined? @@y) ? @@y : :undef, (defined? @@z) ? @@z : :undef] end end p [B.b, C.c, D.d] #=> [[:dx, :undef, :undef], [:undef, :dy, :undef], [:dx, :dy, :dz]] module B @@x = :bx2 @@y = :by2 @@z = :bz2 end p [B.b, C.c, D.d] # 3.0.3 だとここで例外 #=> 3.1.0: [[:bx2, :by2, :bz2], [:undef, :dy, :undef], [:bx2, :dy, :dz]] #=> 2.7.5: [[:bx2, :by2, :bz2], [:undef, :dy, :undef], [:bx2, :dy, :bz2]] module C @@x = :cx2 @@y = :cy2 @@z = :cz2 end p [B.b, C.c, D.d] #=> 3.1.0: [[:bx2, :by2, :bz2], [:cx2, :cy2, :cz2], [:bx2, :cy2, :dz]] #=> 2.7.5: [[:bx2, :by2, :bz2], [:cx2, :cy2, :cz2], [:cx2, :cy2, :cz2]] なななんと、2.7.5 でも 3.1.0 でも例外にならずに完走するが、結果が違う。 びっくりした。 3.1.0 だと完走するのに、3.0.3 だと例外。さらにびっくりした。 びっくりしたし、それぞれどんな理路でそのような出力になったり例外になったりするのかは私には説明できない。 2.7.5 と 3.1.0 で結果が違うことに気づいた時点で真剣に考えるのを放棄したということでもあるけれど。 再度まとめ Python と ruby のクラス変数は、両方ともわりと思いがけないことが起こる。 どちらも気持ち悪いけど、どちらかというとというかわりと圧倒的に ruby の方が気持ち悪い。 ruby のクラス変数はめちゃくちゃ気持ち悪いし、中の人も悩んでいるんだと思うので、継承が絡む局面での利用は避けたほうがいいと思う。