- 投稿日:2019-12-13T22:36:58+09:00
Xamarin.Androidでクライアント証明書を使った認証をしようとした話
まだ解決してません
同じことで躓く人がいなくなりますように書き残します。
要件
Azureサーバーへの通信は特定多数のPC、android端末からのみを許可したい
1番目に試したこと
設定からクライアント証明書をandroid端末にインストールし、
プログラムで参照して、リクエストヘッダに載せたかったできなった原因
http://dsas.blog.klab.org/archives/51995232.html
サイトを要約すると、設定から証明書をインストールした場合、
アプリから参照する方法がない。
アプリからインストールさせればできる。2番目に試したこと
アプリから証明書をインストールさせる。
パスワードはユーザーに入力させ、証明書はファイルアプリ内のフォルダに置き、
初回パスワード入力で、証明書をインストールしたら、証明書を削除する想定。2回目以降は証明書ストアから取得するよう書きました。
実際にインストールも、取り出しもできたのですが、問題がありました。
証明書を端末から取り出した場合のみ、証明書についている秘密鍵がnullになってしまい、サーバーで認証が通りません。
private getCert() { X509Certificate2Collection findResult; using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) { store.Open(OpenFlags.MaxAllowed); findResult = store.Certificates.Find(X509FindType.FindBySubjectName, "subjectname", false); store.Close(); } X509Certificate2 cert; if (findResult.Count == 1) { cert = findResult[0]; } else { X509KeyStorageFlags flags = X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable; Stream fis = Android.App.Application.Context.Assets.Open("certfilename.pfx"); cert = new X509Certificate2(GetByteArrayFromStream(fis), "password",flags); using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) { store.Open(OpenFlags.ReadWrite); store.Add(cert); store.Close(); } } return cert; }解決案あったら教えてください
X509Certificate2.PriveteKeyは明示的に値いれようとすると、エラーになります。
Android特有の権限回りの問題なのか、ソースで解決できる問題なのかもわかってません。
Xamarin.Android触って2日目なので、実はもっと簡単だったりするかもですが、解決したら追記します。
- 投稿日:2019-12-13T21:49:38+09:00
C#のできることできないこと
C#その2アドベンドカレンダー13日目です。
今日はC#できることできないことを書いていきたいと思います。
実は去年C#に触れるということがありました。
Javaがわかる人C++がわかる人なら簡単にできます。
ただ気をつけないといけないのはクラス呼び出しの書き方です。using クラス名になります。
利点は
正直言うとコンストラクタデコンストラクタが使えるということと簡単にかけるといったところですね。
下手したらPython並に簡単にかけます。
Javaみたいに仕様やライセンスに振り回されることもなくなりました。
最近はクロスプラットフォームでも使えます。
それはxamarinとmonoと.net coreの存在です。
これらが増えたおかげで簡単にいろんなプラットフォームにプログラムが提供できるようになりました。
実は我社にはC#大好き人間がいますがxamarinのこと知りません。
今の自分だったらXamarinかPWA(Riot利用の)を考えてしまいますね。
それほどC#は進化したと思っていますb。弱点
実は去年データベースライブラリを自分で作ったのですが。
思ったより使えませんでした。
それは実はPostgreSQLとLINQの組み合わせの相性が最悪でした。
今後頑張って開発していってくれると思っていますが。
オープンオース系データベースとC#の組み合わせが相性悪いです。
これができたら最強な武器の登場になると自分は思っています。今後やってみたいこと
特にほしいといえばMySQLとMongo用のLINQができてくれればすぐにでも使いたいです。
と言うか作ってくださいマイクロソフトさん。
PHPも好きですがC#も大分好きになりました。
去年使うまでは食わず嫌いでしたが本当に好きな言語の一つになってくれました。
PHP・Python・Riot・jQueryと同じくらい好きな言語の一つになってくれました。
いろんな複合技を使わなければいけないときには使いたいと思っています。なんか文書だけになってしまい申し訳ございませんが。
13日目のアドベンドカレンダーといたします。
- 投稿日:2019-12-13T21:43:55+09:00
using を忘れたときの楽な方法
C# で あ~using XXXXXX なんだっけな~
とりあえず書いて、左端のマークをクリック
これで安心。楽でいいよな
----MCK9595さんからアドバイスで追記&検証&説明追加----------
もっと簡単な入力方法がありました。マウス操作なし方法!
例えば、List と文字を打ち込んだら、 ctrl + . enter をすればいいだけ。
マウス操作すら不要。
この場合、Listに相当する using System.Collections.Generic;
が自動追記されます。これはすごく楽!
C++のインクルードとは違う。
C#は引き出しを選ぶだけだから意味合いは微妙に違うけど、
作業的には、プログラムの冒頭に何のキーワードを置くか?だけ。
この自動化をやってくれるのはいい。
- 投稿日:2019-12-13T21:10:53+09:00
C#とAndroidによるUDP通信で加速度センサの値を取得するアプリケーション
大まかな内容
ここでは、Androidの加速度センサの値をC#で取得し、その値によってAndroidの端末がどのように動いたかを感知するために作成している。
Androidのアプリケーション
このアプリケーションでは、三軸(X,Y,Z)の加速度センサの値をUDP通信で送る。端末が、どの軸方向に向いているかを分かりやすくるために、各軸が一定の値以上になった場合に音声が鳴るように作成した。
アプリケーションのGUI
ソースコード
ソケット通信を行うので、Androidmanifest.xmlに下記を追加する。
Androidmanifest.xml<uses-permission android:name="android.permission.INTERNET"></uses-permission>次に、画像と音声ファイルを扱うため、app内のresのdrawableとrawを作成し、その中に画像データと音声データを入れる。
app └res ├drawable │ ├en.jpg │ ├en_gleen.jpg │ └en_red.jpg └raw ├xjikum.mp3 ├xjikup.mp3 ├yjikum.mp3 ├yjikup.mp3 ├zjikum.mp3 └zjikup.mp3activity_main.xml<EditText android:id="@+id/IP_Address" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="52dp" android:layout_marginEnd="16dp" android:ems="10" android:inputType="textPersonName" android:text="0.0.0.0" android:textSize="24sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <EditText android:id="@+id/Port" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="48dp" android:layout_marginEnd="16dp" android:ems="10" android:inputType="textPersonName" android:text="8080" android:textSize="24sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/IP_Address" /> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="280dp" android:text="X:" android:textSize="32sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="24dp" android:text="Y:" android:textSize="32sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textView" /> <TextView android:id="@+id/textView3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="24dp" android:text="Z:" android:textSize="32sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textView2" /> <TextView android:id="@+id/X_Data" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="280dp" android:text="TextView" android:textSize="32sp" app:layout_constraintStart_toEndOf="@+id/textView" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/Y_Data" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:text="TextView" android:textSize="32sp" app:layout_constraintStart_toEndOf="@+id/textView2" app:layout_constraintTop_toBottomOf="@+id/X_Data" /> <TextView android:id="@+id/Z_Data" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:text="TextView" android:textSize="32sp" app:layout_constraintStart_toEndOf="@+id/textView3" app:layout_constraintTop_toBottomOf="@+id/Y_Data" /> <TextView android:id="@+id/textView7" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="230dp" android:text="加速度センサの値" android:textSize="32sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/Ran" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="87dp" android:layout_marginLeft="87dp" android:layout_marginBottom="70dp" android:text="通信開始" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" /> <TextView android:id="@+id/textView4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginLeft="16dp" android:layout_marginBottom="8dp" android:text="IPアドレス" android:textSize="24sp" app:layout_constraintBottom_toTopOf="@+id/IP_Address" app:layout_constraintStart_toStartOf="parent" /> <TextView android:id="@+id/textView5" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginLeft="16dp" android:layout_marginBottom="8dp" android:text="ポート番号" android:textSize="24sp" app:layout_constraintBottom_toTopOf="@+id/Port" app:layout_constraintStart_toStartOf="parent" /> <Button android:id="@+id/End" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="87dp" android:layout_marginRight="87dp" android:layout_marginBottom="70dp" android:text="通信解除" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> <ImageView android:id="@+id/LED1" android:layout_width="75dp" android:layout_height="75dp" android:layout_marginStart="100dp" android:layout_marginLeft="100dp" android:layout_marginBottom="150dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:srcCompat="@drawable/en" /> <ImageView android:id="@+id/LED2" android:layout_width="75dp" android:layout_height="75dp" android:layout_marginEnd="100dp" android:layout_marginRight="100dp" android:layout_marginBottom="150dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:srcCompat="@drawable/en" /> <TextView android:id="@+id/textView6" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="352dp" android:layout_marginEnd="124dp" android:layout_marginRight="124dp" android:text="Delay" android:textSize="32sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> <EditText android:id="@+id/Delay" android:layout_width="159dp" android:layout_height="49dp" android:layout_marginTop="16dp" android:layout_marginEnd="40dp" android:layout_marginRight="40dp" android:ems="10" android:inputType="textPersonName" android:text="100" android:textSize="24sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/textView6" />MainActivity.javaimport android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.media.AudioAttributes; import android.media.AudioManager; import android.media.SoundPool; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetSocketAddress; import java.net.SocketException; import java.util.Timer; import java.util.TimerTask; public class MainActivity extends AppCompatActivity implements SensorEventListener{ private SensorManager sensorManager; private TextView X_Data_TextView; //加速度センサXの値 private TextView Y_Data_TextView; //加速度センサYの値 private TextView Z_Data_TextView; //加速度センサZの値 private float data_X,data_Y,data_Z; private int xp,yp,zp,xm,ym,zm; private int data_x,data_y,data_z; private String Data; private Timer timer1,timer2; private mTimerTask1 timerTask1; private mTimerTask2 timerTask2; private Handler handler = new Handler(); private Handler handler1 = new Handler(); private long Delay; ImageView LED1,LED2; SoundPool soundPool; int mp3_xp,mp3_xm,mp3_yp,mp3_ym,mp3_zp,mp3_zm; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE); X_Data_TextView = findViewById(R.id.X_Data); Y_Data_TextView = findViewById(R.id.Y_Data); Z_Data_TextView = findViewById(R.id.Z_Data); LED1 = findViewById(R.id.LED1); LED2 = findViewById(R.id.LED2); LED1.setImageResource(R.drawable.en); LED2.setImageResource(R.drawable.en); //効果音付けるのに必要なやつ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { soundPool = new SoundPool(5, AudioManager.STREAM_MUSIC, 0); } else { AudioAttributes attr = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build(); soundPool = new SoundPool.Builder() .setAudioAttributes(attr) .setMaxStreams(5) .build(); } mp3_xp = soundPool.load(this, R.raw.xjikup, 1); mp3_xm = soundPool.load(this,R.raw.xjikum,1 ); mp3_yp = soundPool.load(this, R.raw.yjikup, 1); mp3_ym = soundPool.load(this,R.raw.yjikum,1 ); mp3_zp = soundPool.load(this, R.raw.zjikup, 1); mp3_zm = soundPool.load(this,R.raw.zjikum,1 ); Button ran = findViewById(R.id.Ran); Button end = findViewById(R.id.End); ran.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if(null != timer1){ timer1.cancel(); timer1 = null; } xp=yp=zp=xm=ym=zm=0; String delay = ((EditText)findViewById(R.id.Delay)).getText().toString(); Delay = Long.parseLong(delay); timer1 = new Timer(); timerTask1 = new mTimerTask1(); timer1.schedule(timerTask1,0, Delay); timer2 = new Timer(); timerTask2 = new mTimerTask2(); timer2.schedule(timerTask2,0,Delay); } }); end.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if(null != timer1){ timer1.cancel(); timer1 = null; } final String address = ((EditText) findViewById(R.id.IP_Address)).getText().toString(); String port = ((EditText) findViewById(R.id.Port)).getText().toString(); int Port = Integer.parseInt(port); String exit = "exit"; byte buf[] = new byte[exit.length()]; try { buf = exit.getBytes("SHIFT_JIS"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } InetSocketAddress inetSocketAddress = new InetSocketAddress(address, Port); final DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length, inetSocketAddress); AsyncTask<DatagramPacket, Void, Void> task = new AsyncTask<DatagramPacket, Void, Void>() { @Override protected Void doInBackground(DatagramPacket... datagramPackets) { DatagramSocket datagramSocket = null; try { datagramSocket = new DatagramSocket(); datagramSocket.send(datagramPackets[0]); datagramSocket.close(); } catch (SocketException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } }; task.execute(datagramPacket); } }); } @Override protected void onResume(){ super.onResume(); //Event Listener登録 Sensor accel = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); sensorManager.registerListener((SensorEventListener) this,accel,SensorManager.SENSOR_DELAY_NORMAL); } @Override protected void onPause(){ super.onPause(); //Event Listener登録解除 sensorManager.unregisterListener((SensorEventListener) this); } @Override public void onSensorChanged(SensorEvent event){ if(event.sensor.getType() == Sensor.TYPE_ACCELEROMETER){ data_X= (500+event.values[0]*25); data_Y= (500+event.values[1]*25); data_Z= (500+event.values[2]*25); data_x = (int)data_X; data_y = (int)data_Y; data_z = (int)data_Z; Data = data_x + " " + data_y + " " + data_z; X_Data_TextView.setText(String.valueOf(data_x)); Y_Data_TextView.setText(String.valueOf(data_y)); Z_Data_TextView.setText(String.valueOf(data_z)); } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy){ } private class mTimerTask2 extends TimerTask{ @Override public void run() { handler.post(new Runnable() { @Override public void run() { if (data_X >= 700) { LED1.setImageResource(R.drawable.en_gleen); xp += 1; if (xp == 1) { yp=zp=xm=ym=zm=0; soundPool.play(mp3_xp, 2, 2, 0, 0, 1f); } }else if (data_X <= 300) { LED1.setImageResource(R.drawable.en); xm += 1; if (xm == 1) { xp=yp=zp=ym=zm=0; soundPool.play(mp3_xm, 2, 2, 0, 0, 1f); } }else if (data_Y >= 700) { LED2.setImageResource(R.drawable.en_red); yp += 1; if (yp == 1) { xp=zp=xm=ym=zm=0; soundPool.play(mp3_yp, 2, 2, 0, 0, 1f); } } else if (data_Y <= 300) { LED2.setImageResource(R.drawable.en); ym += 1; if (ym == 1) { xp=yp=zp=xm=zm=0; soundPool.play(mp3_ym, 2, 2, 0, 0, 1f); } }else if (data_Z >= 700) { LED1.setImageResource(R.drawable.en_gleen); LED2.setImageResource(R.drawable.en_red); zp += 1; if (zp == 1) { xp=yp=xm=ym=zm=0; soundPool.play(mp3_zp, 2, 2, 0, 0, 1f); } }else if (data_Z <= 300) { LED1.setImageResource(R.drawable.en); LED2.setImageResource(R.drawable.en); zm += 1; if (zm == 1) { xp=yp=zp=xm=ym=0; soundPool.play(mp3_zm, 2, 2, 0, 0, 1f); } } } }); } } private class mTimerTask1 extends TimerTask{ @Override public void run(){ handler.post(new Runnable() { @Override public void run() { final String address = ((EditText) findViewById(R.id.IP_Address)).getText().toString(); String port = ((EditText) findViewById(R.id.Port)).getText().toString(); int Port = Integer.parseInt(port); byte buf[] = new byte[Data.length()]; try { buf = Data.getBytes("SHIFT_JIS"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } InetSocketAddress inetSocketAddress = new InetSocketAddress(address, Port); final DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length, inetSocketAddress); AsyncTask<DatagramPacket, Void, Void> task = new AsyncTask<DatagramPacket, Void, Void>() { @Override protected Void doInBackground(DatagramPacket... datagramPackets) { DatagramSocket datagramSocket = null; try { datagramSocket = new DatagramSocket(); datagramSocket.send(datagramPackets[0]); datagramSocket.close(); } catch (SocketException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } }; task.execute(datagramPacket); } }); } } }C#のアプリケーション
このアプリケーションでは、UDP通信で受け取ったAndroidの加速度センサの値をrichTextBoxに記録し、各軸の値をそれぞれTextBox表示するように作成した。
アプリケーションのGUI
ソースコード
Form1.csusing System; using System.Windows.Forms; using System.Net.Sockets; using System.Net; namespace UDP_Server { public partial class Form1 : Form { int i; int[] d; string[] D; private UdpClient udpClient = null; //受信用クライアント public string rcvMsg = null;//受信メッセージ格納用 public Form1() { InitializeComponent(); } //ボタンをクリックしたときの処理 private void button1_Click(object sender, EventArgs e) { //UDPの接続があるときにボタンを押した場合 //その処理をなかったことにする(少し違う) if (udpClient != null) { return; } ((Button)sender).Enabled = false; string IPString = "127.0.0.1"; IPAddress IPAdd = IPAddress.Parse(IPString); //IPアドレスを指定 int Port = 8080; //ポート番号を指定 //UdpClientを作成し、指定したポート番号にバインドする IPEndPoint EP = new IPEndPoint(IPAdd, Port); UdpClient udp = new UdpClient(EP); richTextBox1.BeginInvoke( new Action<string>(ShowReceivedString1), "受信を開始します"); //非同期的な受信を開始する udp.BeginReceive(UdpServer, udp); ((Button)button1).Enabled = true; } //データを受信した時の処理 private void UdpServer(IAsyncResult ar) { UdpClient udp = (UdpClient)ar.AsyncState; for (;;) { //一度非同期受信を終了する IPEndPoint remoteEP = null; byte[] rcvBytes = udp.Receive(ref remoteEP); //受信したデータを文字列に変換 string rcvMsg = System.Text.Encoding.UTF8.GetString(rcvBytes); //"exit"を受信したら終了 if (rcvMsg == "exit") { break; } D = rcvMsg.Split(' '); for(i=0;D[i]== null; i++) { d[i] = int.Parse(D[i]); } //受信したデータをTextBoxに表示する string displayMsg = string.Format("{0}", rcvMsg); richTextBox1.BeginInvoke( new Action<string>(ShowReceivedString1), displayMsg); X_Data.BeginInvoke( new Action<string>(ShowReceivedString_x), D[0]); Y_Data.BeginInvoke( new Action<string>(ShowReceivedString_y), D[1]); Z_Data.BeginInvoke( new Action<string>(ShowReceivedString_z), D[2]); } rcvMsg = "終了しました"; //UdpClientを閉じる udp.Close(); richTextBox1.BeginInvoke( new Action<string>(ShowReceivedString1), rcvMsg); } private void ShowReceivedString1(string str) { if (richTextBox1.Text == "") { richTextBox1.Text = str; richTextBox1.SelectionStart = richTextBox1.Text.Length; richTextBox1.Focus(); richTextBox1.ScrollToCaret(); } else { richTextBox1.Text = richTextBox1.Text + "\r\n" + str; richTextBox1.SelectionStart = richTextBox1.Text.Length; richTextBox1.Focus(); richTextBox1.ScrollToCaret(); } } private void ShowReceivedString_x(string str) { X_Data.Text = str; } private void ShowReceivedString_y(string str) { Y_Data.Text = str; } private void ShowReceivedString_z(string str) { Z_Data.Text = str; } private void Form1_FormClosed(object sender, FormClosedEventArgs e) { //UdpClientを閉じる if (udpClient != null) { udpClient.Close(); } } } }今後の目標
C#に加速度センサの値で制御できるLEDを模した画像を追加しようと思っているが、そのためにはマルチスレッドでpictureBoxを操作できるようにならないといけないのでそこに力を入れていく。
- 投稿日:2019-12-13T20:49:37+09:00
プログラミング初心者のpaizaのC#コースメモ1日目
この記事は?
プログラミング初心者がpaizaさんのC#講座を少しづつ進めていく日記
paiza https://paiza.jp/
わからなかったこととか調べたことをメモします。今日やったこと
- C#入門編1: プログラミングを学ぶ (全9回)
- C#入門編2: 条件によって処理を変えてみよう (全6回)
詰まったこと
メソッドってなに?
→ステートメント(〇〇;とか〇〇(){};)をまとめたもの
以下自分が調べた順に掲載
メソッド - C# ガイド | Microsoft Docsより
https://docs.microsoft.com/ja-jp/dotnet/csharp/methodsメソッドは、一連のステートメントが含まれているコード ブロックです。 必要なメソッド引数を指定してプログラムからメソッドを呼び出すと、メソッド内のステートメントが実行されます。 C# では、実行されるすべての命令がメソッドのコンテキストで実行されます。 Main メソッドは、すべての C# アプリケーションのエントリ ポイントです。プログラムが開始されると、このメソッドが共通言語ランタイム (CLR) によって呼び出されます。
何をいっているのか理解できなかった。
もしかして日本語訳しているからわからないのか?
→Englishだけで何パターンあるの…?English (Australia) English (Canada) English (India) English (Ireland) English (Malaysia) English (New Zealand) English (Singapore) English (South Africa) English (United Kingdom) English (United States)
それでも食らいついてみる。
English(UK)
https://docs.microsoft.com/en-gb/dotnet/csharp/methodsA method is a code block that contains a series of statements. A program causes the statements to be executed by calling the method and specifying any required method arguments. In C#, every executed instruction is performed in the context of a method. The Main method is the entry point for every C# application and it is called by the common language runtime (CLR) when the program is started.
ステートメントって言葉が大事そう。
ただ、自分が知っているstatementの意味だと文章が理解できない。
こんなとき英語力のなさが恨めしい。ステートメント(weblio)
https://ejje.weblio.jp/content/statement述べること、(文書・口頭による)陳述、声明、供述、陳述、声明書、ステートメント、計算書、貸借表、陳述文
自分が知っている意味以上のものは出てこなかった。
仕方がないのでMicroSoftのページで調べ直し。ステートメント (C# プログラミング ガイド)| Microsoft Docs
https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/statements-expressions-operators/statementsプログラムが実行する処理は、ステートメントとして表されます。 一般的な処理には、変数の宣言、値の代入、メソッドの呼び出し、コレクションに対するループ処理、条件に応じたコード ブロックへの分岐などがあります。 プログラム内でステートメントが実行される順序は、制御フローまたは実行フローと呼ばれます。 制御フローは、実行時に渡された入力に対するプログラムの応答に応じて、プログラムを実行するたびに変わる可能性があります。
ステートメントは、セミコロンで終わる単一行のコードか、1 つのブロックを形成する一連の単一行ステートメントで構成されます。 ステートメント ブロックは中かっこ {} で囲み、入れ子になったブロックを含めることができます。 次のコードは、2 つの単一行ステートメントの例と、1 つの複数行ステートメント ブロックを示しています。
Statements (C# Programming Guide)
https://docs.microsoft.com/en-gb/dotnet/csharp/programming-guide/statements-expressions-operators/statementsThe actions that a program takes are expressed in statements. Common actions include declaring variables, assigning values, calling methods, looping through collections, and branching to one or another block of code, depending on a given condition. The order in which statements are executed in a program is called the flow of control or flow of execution. The flow of control may vary every time that a program is run, depending on how the program reacts to input that it receives at run time.
A statement can consist of a single line of code that ends in a semicolon, or a series of single-line statements in a block. A statement block is enclosed in {} brackets and can contain nested blocks. The following code shows two examples of single-line statements, and a multi-line statement block:
初心者にとって重要なのは後半部分。(というか前半は理解できていない。)
statement can consist of a single line of code that ends in a semicolon, or a series of single-line statements in a block.
講座で入力していた〇〇;とか〇〇(){};のことをステートメントと言うらしい。
一番最初に戻って、それらが集まったブロックをメソッドと呼び、contextは順番にって意味だろう。
Mainメソッドがentry pointになるってことはそこから開始しますよって事だろう。
CLRは調べても理解しきれなかったので後日まとめる。→宿題piazaで書いていたコードを見返してみる。
//こんな感じでpiazaでは書いてました。 using System; public class Program{ public static void Main(){ //ここらへんに課題のコードを書いていく } }多分、
public class Program{
と}
の間にあるものがメソッド?
そすると次はメソッドとクラスの違いがわからぬ。class (C# リファレンス)
https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/classクラスは、次の例に示すように、キーワード class を使用して宣言します。
class (C# Reference)
https://docs.microsoft.com/en-gb/dotnet/csharp/language-reference/keywords/classClasses are declared using the keyword class, as shown in the following example:
なんてことだ。使い方しか書いていない。
ダメ元で見てみた英語も具体例しか載っていない。
検索窓で探してみるとそれっぽいのを発見。
→だめでした。クラスおよびオブジェクトを使用したオブジェクト指向プログラミングについて確認します
https://docs.microsoft.com/ja-jp/dotnet/csharp/tutorials/intro-to-csharp/introduction-to-classesこのチュートリアルでは、開発用に使用できるマシンがあることを想定しています。 Windows、Linux、または macOS 上でローカルの開発環境を設定する手順については、.NET チュートリアル Hello World in 10 minutes (10 分で Hello World) に記載されています。 使用するコマンドの概要については、詳細な情報へのリンクが掲載されている、開発ツールに対する理解を深める方法に関するページをご覧ください。
その下のクラスとオブジェクトが正解でした。
クラスとオブジェクト
https://docs.microsoft.com/ja-jp/dotnet/csharp/tour-of-csharp/classes-and-objects"クラス" は C# の最も基本的な型です。 クラスは、状態 (フィールド) とアクション (メソッドおよびその他の関数メンバー) を 1 つの単位としてまとめたデータ構造です。 クラスは動的に作成された "インスタンス" の定義を提供し、"オブジェクト" とも呼ばれます。 クラスでは、"継承"と "ポリモーフィズム" をサポートします。これによって "派生クラス" が "基底クラス" を拡張して特殊化できます。
新しいクラスはクラス宣言を使用して作成されます。 クラス宣言は、クラスの属性と修飾子、クラスの名前、基底クラス (指定されている場合)、およびクラスによって実装されるインターフェイスを指定するヘッダーで開始します。 ヘッダーの後にはクラス本体が続きます。これは、区切り記号 { と } の間に記述するメンバー宣言のリストで構成されます。
日本語だと解釈に悩むところがあるのでUK版も。
Classes and objects
https://docs.microsoft.com/en-gb/dotnet/csharp/tour-of-csharp/classes-and-objectsClasses are the most fundamental of C#’s types. A class is a data structure that combines state (fields) and actions (methods and other function members) in a single unit. A class provides a definition for dynamically created instances of the class, also known as objects. Classes support inheritance and polymorphism, mechanisms whereby derived classes can extend and specialize base classes.
New classes are created using class declarations. A class declaration starts with a header that specifies the attributes and modifiers of the class, the name of the class, the base class (if given), and the interfaces implemented by the class. The header is followed by the class body, which consists of a list of member declarations written between the delimiters { and }.
状態とメソッドを一括にしたものがクラス。
ここまではいい。
オブジェクトと呼ばれているものはなんぞ?
クラス自体がそう呼ばれているのか、インスタンスを作るものなのか。
自分の英語力ではわからなかった。→宿題クラスの説明を読むに、さっきのメソッドの解釈は間違っていそう。
[×]public class Program{
と}
の間にあるものがメソッド
[○]public static void Main(){
から}
までにある括りがメソッド
で、クラスの中にはメソッドが複数入ることもある…?
(methods
になっている)結局Console.Write(Line)って何?
→わかりませんでした!
Console クラス
https://docs.microsoft.com/ja-jp/dotnet/api/system.console?view=netframework-4.8コンソール アプリケーションの標準入力ストリーム、標準出力ストリーム、および標準エラー ストリームを表します。 このクラスは継承できません。
Console Class
https://docs.microsoft.com/en-gb/dotnet/api/system.console?view=netframework-4.8Represents the standard input, output, and error streams for console applications. This class cannot be inherited.
短いのに理解できない。
ストリーム?Stream クラス
https://docs.microsoft.com/ja-jp/dotnet/api/system.io.stream?view=netframework-4.8バイト シーケンスのジェネリック ビューを提供します。 これは抽象クラスです。
Stream Class
https://docs.microsoft.com/en-gb/dotnet/api/system.io.stream?view=netframework-4.8Provides a generic view of a sequence of bytes. This is an abstract class.
バイトシーケンスについて調べようと思ったら、ジェネリック ビューまで検索候補に出てくる。
みんなわからないのだと少し安心した。
というかスペースまで再現しているってことはコピペで検索してるな。と思ったら注釈に概要が書いてあった。
Streamは、すべてのストリームの抽象基本クラスです。 ストリームとは、ファイル、入出力デバイス、プロセス間通信のパイプ、または TCP/IP ソケットなどのバイト シーケンスを抽象化したものです。 クラスStreamとその派生クラスは、これらのさまざまな種類の入力と出力の汎用ビューを提供し、プログラマとオペレーティングシステムの固有の詳細および基になるデバイスを分離します。
抽象基本クラスとやらはわからないが、データのやり取りに使うよってことだと思う。
ConsoleがクラスでStreamもクラスってことはクラスの中にクラスが入れ込むことがあるってこと?→宿題
それともストリームって言葉を調べたけど、Streamクラスとは別物で調べ直しが必要なのか?脱線してる気がする。
Consoleがクラスならば状態とメソッドがある。
そのメソッドの中にWrite,WriteLineが含まれているらしい。Console クラス
https://docs.microsoft.com/ja-jp/dotnet/api/system.console?view=netframework-4.8#methodsでもWriteとWriteLineだけでも種類が多すぎて追いつかない。
諦める。→宿題varって何?
→あとから変数の型を推測してくれるっぽい。使えるなら使ったほうがよさそう。
Qiitaを使ってるんだから、そこから探してみた。
var の使いどころ
https://qiita.com/hollydad/items/1516a3f13147a754b9db
var(型推論)の利用指針について
https://qiita.com/Nossa/items/e5e50d2fc1ae8b3e2389
C# のコーディング規則 (C# プログラミング ガイド)
https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/inside-a-program/coding-conventions#implicitly-typed-local-variablesあとから見直すことを考えたら変数名で推測できるようにしたほうがよさそう。
クラスとインスタンス
→宿題
データの種類
→宿題
宿題として残したもの
- CLRは調べても理解しきれなかったので後日まとめる。
- オブジェクトと呼ばれているものはなんぞ?
- ConsoleがクラスでStreamもクラスってことはクラスの中にクラスが入れ込むことがあるってこと?
- WriteとWriteLineだけでも種類が多すぎて追いつかない。
- ラスとインスタンス
- データの種類
- 投稿日:2019-12-13T19:21:39+09:00
【Unity】素早くUnity OculusQuest対応アプリを作ってみる/UnitySettingSwitcher使う版
せっかくUnitySettingSwitcher作ったので実際に使ってみるチュートリアルを書きました。
今回は新規にOculusQuest向けプロジェクトを作成する手順です。前提
- 実行環境
- Unity 2018.4.12f
- Android 8.1(Oreo, 27) SDK (Android向けのビルド環境がセットアップ済であること)
- macOS Catalina version 10.15.1
- Oculus Quest
- 開発者モードが有効になっていること
- USBデバッグを許可していること
- 使用アセット
- OculusIntegration v1.43
- UnitySettingSwitcher v0.0.5 (https://github.com/uisawara/unity-settingswitcher/releases)
- MiniJSON (https://gist.github.com/darktable/1411710)
手順
- プロジェクト作成・アセットインポート
- 新規プロジェクトを作成
- AssetStoreからOculusIntegrationをimport
- MiniJSONをimport
- UnitySettingSwitcherをimport
- プロジェクトの設定
- 設定ファイル /Assets/Settings.xr.json を作成(内容は下方に記載)
- 設定を適用
- メニュー Window/UnitySettingsSwitcherウィンドウを開く(CMD+E)
- "Reload"ボタンで設定ファルをリロード
- "targets/vr-oculus"をクリックしてOculus向け設定を反映 (テンプレコピー→ボタン1クリックで設定が終わる)
- サンプルシーンの作成
- /Assets/Oculus/VR/Prefabs/OVRCameraRigをHierarchyにD&Dして配置
- OVRCameraRigをInspectorで開き OVRManagerのTarget DevicesにQuestを設定
- ビルドと実行
- メニュー File/BuildAndRunでBuild Settingsウィンドウを開く
- "Build And Run"ボタンクリックでアプリをビルド・実行
/Assets/Settings.xr.json{ "settings": [ { "name": "targets/vr-oculus", "player_settings": { "virtualRealitySupported": true, "VirtualRealitySDKs": "Oculus", }, "editor_user_build_settings": { "androidBuildSubtarget": "ASTC" }, "xr_settings": { "enabled": true }, "android": { "minSdkVersion": "AndroidApiLevel22", } } ] }結果
これでいつもの青い空間がQuestで見られる筈
補足: UnitySettingSwitcherの導入で簡素になったところ
Settings.xr.jsonの設定テンプレートを使うことで以下の手順が簡略化されています。
- PlayerSettings
- virtualRealitySupportedを有効に設定
- VirtualRealitySDKsにOculusを追加
- EditorUserBuildSettings
- "androidBuildSubtarget"に"ASTC"を設定
- android
- "minSdkVersion"を"AndroidApiLevel22"に設定
あとQiitaに記事書くときUnityの設定変更していく手順を全部省略してSettingファイルの内容貼り付ければ
設定紹介できるのでそこ便利。。引用
- OculusQuest向けのプロジェクト作成の手順については、こりんさん @korinVR の記事を参考にさせていただきました、感謝。
- UnitySettingSwitcherについての紹介記事はこちら
- 投稿日:2019-12-13T17:47:21+09:00
VS2019でC#でコンソールアプリをはじめて作る
C#でコンソールアプリを作ろうとする・・・
すると、一見すると
.net framework と .net core でコンソールアプリどちらでも作れそうな・・・
また調べようとすると、見つからないか、分かりにくい、で時間を取らるのも馬鹿馬鹿しい。
使えればいいのだ。
調べればわかる、だけではなく、即効性ですぐにやりたい。プログラム以外に別件でやりたいことはたくさんある。
で、検索方法を頭から考え直す。
チュートリアルを探せばいいだけだ。
あった。
https://docs.microsoft.com/ja-jp/visualstudio/get-started/csharp/tutorial-console?view=vs-2019.net core を使えばいいようだ。
VisualStudio 2010 のC#ではとかではそういうモードがなかったような?
まあ YAGNIベースで考えよう
あああ?C#の新規作成のつもりが、F#になってる?
目が悪いのか?確認
F#になっている、なんか操作ミスしていたようだ。ところで前からF#あるけど、使っている人を聞いたことがない。
Qiitaは画像の貼り付けが簡単でいい!いや~楽だ!貼り付け利便性の良さはすばらしい!とりあえず、再挑戦とミスの確認
F#はメニューに無かったが、マウスがずれてLinuxになったか、まちがえてコンソールを選んだか、どちらかだろう。いやそんなことはない。カーソルがまとまったこれ一択なので違う。
うまくいっているようにみえるが、とりあえず作成ボタンを押す
Hello World!
うまくいった。2010,2012,2013 でC#を使った時より若干分かりにくくなったような
そういえばUnityで2015も使っていたか・・・忘れてた
なぜはじめにやったときF#のプロジェクト作成になったか?
全く同じ操作をしたつもりだったし謎すぎる????
- 投稿日:2019-12-13T17:39:17+09:00
UIElementsで開発するときの問題と解決
PONOS Advent Calendar 2019の13日目の記事です。
昨日は@nimitsuさんのGameLift RealTimeServerで遊んでみよう for Unity(Unity編)でした。
はじめに
Unity 2019.1でついにUIElementsが正式リリースされました。
今後ランタイムUIへの採用も決定されており、Unity全体のUIフレームワークとして利用されていくことになると思うのですが、残念ながら現時点ではまだ使いづらさを感じることがあります。今回は私がUIElementsに触れてみて不便だと感じた問題点を共有するとともに、皆様のUIElements開発が少しでも改善されるようにそれらの問題点の解消方法を紹介したいと思います。
※もしこれから紹介する問題点について、他の解消方法をご存じの方がいらっしゃいましたらコメントください!
※紹介している以外に困っている点がありましたらぜひコメントください。一緒に解決しましょう!なお、
- Unity 2019.2.12f1
- MacOS 10.14.6
の環境で動作確認しています。
UXMLをリロードできるようにする
問題
公式のコードや
Assets/Create/UIElements Editor Window
メニューで作られるコード上では、EditorWindowのOnEnable()内でUXMLやUSSを読み込み、反映するように記述されています。
EditorWindowがフォーカスされるたびにレイアウトを読み込み直す必要はないですし、EditorWindowが開いたタイミングかコンパイルが走ったタイミングでこれらの処理が実行されるのは無駄がなく効率的だと思います。しかし、開発中などはUXMLの編集を頻繁に行いますし、コンパイルが走るタイミングとは違うタイミングでUXMLを読み込み直したいことも多いと思います。
もちろん、そのたびにEditorWindowを開き直せば再度UXML の読み込むことができますが、フローティングなウインドウならともかく、他のウインドウとドッキングして使用することを想定したウインドウの場合にはEditorWindowの開き直しは結構な手間です。(なお、USSはリアルタイムで変更が反映されます)
解決
EditorWindowのメニューからUXMLのリロードを行えるようにしました。
EditorWindowにIHasCustomMenuを実装し、リロードするためのメニューアイテムを登録するようにしています。using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; using UnityEngine.UIElements; public class ReloadableEditorWindow : EditorWindow, IHasCustomMenu { public void AddItemsToMenu(GenericMenu menu) { // メニューアイテムを登録。 menu.AddItem(new GUIContent("Reload"), false, () => { ReloadUxml(); }); } [MenuItem("Window/UIElements/ReloadableEditorWindow")] static void Open() { ReloadableEditorWindow wnd = GetWindow<ReloadableEditorWindow>(); wnd.titleContent = new GUIContent("ReloadableEditorWindow"); } public void OnEnable() { ReloadUxml(); } void ReloadUxml() { // 一度ルートに紐付けられた要素を全て削除する。 rootVisualElement.Clear(); // UXMLの読み込み。 var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("..."); VisualElement labelFromUXML = visualTree.CloneTree(); rootVisualElement.Add(labelFromUXML); // その他、レイアウトに関する各種処理。 } }UXMLの編集を行った後にこのメニュー項目を実行すれば、現在開いているEditorWindow上でUXMLを再読み込みさせることができます。
UXML / USSのパスを取得しやすくする
問題
現在UXMLとUSSは、
Assets/
から始まるアセットパスをAssetDatabase.LoadAssetAtPath<T>()
に渡して、それぞれVisualTreeAssetオブジェクトとStyleSheetオブジェクトとしてロードします。
現状で読み込みの機構が整備されていないためかもしれませんが、ソースファイルのパスをハードコーディングしているので、ファイルの場所が変更されたときなどにサポートしづらく、作成したUIElementsをモジュール化して他者へ配布するときにも不便です。var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Sample/SampleWindow.uxml"); var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/Sample/SampleWindow.uss");解決
動的にUXMLとUSSのパスを取得します。
指定したファイル名のファイルをAssetDatabaseから探し、アセットパスを返すメソッドを作成します。
ファイル名から検索するので、後ほどプロジェクトの階層が変わってもファイル名やクラス名が変更されていなければ引き続き参照ができます。using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; using System.Linq; using System.IO; public static class UIElementsPath { public static string FindUxml(string name) { return Find(name, "uxml"); } public static string FindUss(string name) { return Find(name, "uss"); } static string Find(string name, string extension) { var assetHashs = AssetDatabase.FindAssets(name); var results = assetHashs .Select(hash => AssetDatabase.GUIDToAssetPath(Path.GetFileNameWithoutExtension(hash))) .Where(assetPath => Path.GetFileNameWithoutExtension(assetPath) == name) .Where(assetPath => Path.GetExtension(assetPath) == "." + extension); // 同じ名前のUIElementsが複数存在する(名前空間が違う等)場合の警告。 if (1 < results.Count()) { Debug.LogWarning($"\"{name}\"で{results.Count()}件の結果が見つかりました。\n\n{results.Aggregate((a, b) => a + "\n" + b)}"); } return results.FirstOrDefault(); } }このメソッドを使用すると、先程のコードは以下のように書き換えることができます。
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UIElementsPath.FindUxml("SampleWindow")); var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>((UIElementsPath.FindUss("SampleWindow"));おわりに
まだ開発を効率よく進めていくには多少の工夫が必要なUIElementsですが、はじめにお伝えしたとおり将来のUnityのUIはこのフレームワークを利用して開発していくことになるので、早めに触れておくことをオススメします。
明日は@karizumaiさんです!
- 投稿日:2019-12-13T16:30:58+09:00
C#で経路探査アルゴリズムを作ってみた
完成図
はじめに
映画「メイズ・ランナー」を見ていたら、ふと経路探査アルゴリズムを作りたくなってしまいました。
2Dでブロックを描画できればよかったので、ぱっと思い浮かんだのはUnityエンジンでした。
しかし、
ご覧の通り、僕のPCにはもうUnityが入る隙がありませんでした。
そこで、致し方なくWinFormに描画することにしました。描画方法
具体的には、WinFormに
PictureBox
を配置してBitmap
に描きます。pictureBox1.Image = World.bitmap;座標管理
今回は2Dで描画するので、各タイルのとりえる座標は x, y です。
これをより扱いやすくする為に2次元ベクトルとしてクラス化しました。また、例えば2つのベクトルを計算したい時に、
Vector2 v1 = new Vector2(1, 1); Vector2 v2 = new Vector2(1, 1);v1.x = v1.x + v2.x; v1.y = v1.y + v2.y;とハードコーディングするのはしんどいですし、ミスも誘発しそうです。
ということで、演算子をオーバーロードしました。Vector2 result = new Vector2(); result = v1 + v2;完全なコードはGithubにもあります。
Github | Vector2.csclass Vector2 { //////////////////////////////////////////////////////////////////////// // 2次元ベクトルにおける X座標とY座標を保持 //////////////////////////////////////////////////////////////////////// public int x { get; set; } public int y { get; set; } //////////////////////////////////////////////////////////////////////// // コンストラクタ //////////////////////////////////////////////////////////////////////// public Vector2(int _x_, int _y_) { this.x = _x_; this.y = _y_; } //////////////////////////////////////////////////////////////////////// // 加法演算子のオーバーロード //////////////////////////////////////////////////////////////////////// public static Vector2 operator +(Vector2 n1, Vector2 n2) { return new Vector2(n1.x + n2.x, n1.y + n2.y); } //////////////////////////////////////////////////////////////////////// // 減法演算子のオーバーロード //////////////////////////////////////////////////////////////////////// public static Vector2 operator -(Vector2 n1, Vector2 n2) { return new Vector2(n1.x - n2.x, n1.y - n2.y); } //////////////////////////////////////////////////////////////////////// // 乗算演算子のオーバーロード //////////////////////////////////////////////////////////////////////// public static Vector2 operator *(Vector2 n1, Vector2 n2) { return new Vector2(n1.x * n2.x, n1.y * n2.y); } //////////////////////////////////////////////////////////////////////// // 2点の座標における距離を整数で取得 //////////////////////////////////////////////////////////////////////// public static int Distance(Vector2 v1, Vector2 v2) { return (int)Math.Sqrt(Math.Pow(v1.x - v2.x, 2) + Math.Pow(v1.y - v2.y, 2)); } //////////////////////////////////////////////////////////////////////// // 2点の座標における距離を小数点で取得 //////////////////////////////////////////////////////////////////////// public static double DistanceEx(Vector2 v1, Vector2 v2) { return Math.Sqrt(Math.Pow(v1.x - v2.x, 2) + Math.Pow(v1.y - v2.y, 2)); } }ワールド管理
思ったよりソースコードが巨大になってしまったので、簡易的に書きます。
完全なコード: Github | World.csタイルの保持
タイルは配列に保持しておきます。
private static TileBlock[,] tileBlocks { get; set; }イニシャライズ
1.コンテキストの作成
public static void CreateContext() { graphics = Graphics.FromImage(GetBitmap()); if(graphics != null) { LoggerForm.WriteSuccess("Context created."); } else { LoggerForm.WriteError("CreateContext() failed."); } }2.マップの生成
タイルを保持するための配列のメモリ確保と
ビットマップ上におけるタイルサイズの定義を行います。
そして、空のタイルを敷き詰めておきます。public static void CreateMap() { tileBlocks = new TileBlock[MAX_COORD_X, MAX_COORD_Y]; tileSizeX = (bitmap.Width) / MAX_COORD_X; tileSizeY = (bitmap.Height) / MAX_COORD_Y; for (int x = 0; x < MAX_COORD_X; x++) { for (int y = 0; y < MAX_COORD_Y; y++) { AddTile(new Vector2(x, y), TileType.Walkable); } } LoggerForm.WriteSuccess("Map created."); }タイルの管理
タイルには座標だけでなく探索する際に使用するデータやその他色々な値を格納しておきたかったので、
構造体と挙列型を用意しました。完全なコード: GitHub | TileBlock.cs
//////////////////////////////////////////////////////////////////////// // タイル属性を示すEnum //////////////////////////////////////////////////////////////////////// public enum TileType : int { Walkable = 1, Wall = 2, StartTile = 3, GoalTile = 4, NullTile = 5, AnalyzedTile = 6 }//////////////////////////////////////////////////////////////////////// // 探索に使用する属性を示すEnum //////////////////////////////////////////////////////////////////////// public enum AnalyzeAttributes : int { INull = 0x00000000, IOpenedTile = 0x00000030, IClosedTile = 0x00000040, }//////////////////////////////////////////////////////////////////////// // 探索データ構造体 //////////////////////////////////////////////////////////////////////// public struct AnalyzeData { public int コスト; public int 推定コスト; public int スコア; }//////////////////////////////////////////////////////////////////////// // タイル構造体 //////////////////////////////////////////////////////////////////////// private struct TileStruct { public Vector2 coordinate; public TileType tileType; public AnalyzeAttributes attributes; public AnalyzeData analyzeData; public bool isAnalyzed; }描画関連
タイルの描画位置
まず初めに、今回使用するビットマップのサイズ$BS$は
360×360
です。
また、タイルの最大座標$MS$は25×25
です。このときビットマップ上に於けるタイルの縦/横描画サイズ$S$は
S(x,y) = \frac{BS}{MS}マップの周りの余白$w$は
w = \frac{BS-S(x,y)}{2}タイルの位置(左上角)$L$、余白$w$は
L = S(x,y)+wです。
これを関数で表すとtileSizeX = bitmap.Width / MAX_COORD_X; tileSizeY = bitmap.Height / MAX_COORD_Y; public static Size GetTileSize() { return new Size(tileSizeX, tileSizeY); } public static Vector2 GetWhiteSpace() { var x = (bitmap.Width - (GetTileSize().Width * MAX_COORD_X)) / 2; var y = (bitmap.Height - (GetTileSize().Height * MAX_COORD_Y)) / 2; return new Vector2(x, y); } public static Point GetRednerTileLocation(Vector2 coord) { Vector2 drawCoord = new Vector2(tileSizeX * coord.x, tileSizeY * coord.y); return new Point(drawCoord.x + GetWhiteSpace().x, drawCoord.y + GetWhiteSpace().y); }となります。
マウス座標からタイル座標を取得
ビットマップ上のマウス座標x,yからタイル座標x,yを算出します。
結果のタイル座標$cx,cy$は
\begin{align} ax = x+w\\ ay = y+h \end{align}cxのとりえる値の範囲は x \leqq cx \leqq ax にあるタイル\\&&\\ cyのとりえる値の範囲は y \leqq cy \leqq ay にあるタイルであると言えます。
これを関数で表すとpublic static Vector2 GetTileCoordByMouseCoord(Vector2 coord) { for(int x = 0; x < MAX_COORD_X; x++) { for(int y = 0; y < MAX_COORD_Y; y++) { var tileLocation = GetRednerTileLocation(new Vector2(x, y)); var tileSize = GetTileSize(); var xX = tileLocation.X; var Xx = tileLocation.X + tileSize.Width; var yY = tileLocation.Y; var Yy = tileLocation.Y + tileSize.Height; if(xX <= coord.x && Xx >= coord.x) // x ≦ cx ≦ax { if(yY <= coord.y && Yy >= coord.y)// y ≦ cy ≦ay { return new Vector2(x, y); } } } } return new Vector2(0, 0); }となります。
コッチの方が美しいですねif(xX <= coord.x && coord.x <= Xx) // x ≦ cx ≦ax { if(yY <= coord.y && coord.y <= Yy)// y ≦ cy ≦ay { return new Vector2(x, y); } }アルゴリズム
スタートからゴールへの方角を算出
//////////////////////////////////////////////////////////////////////// // 方角を示すEnum //////////////////////////////////////////////////////////////////////// public enum ArrowVector { NULL , UP , DOWN , RIGHT , LEFT , UP_RIGHT , UP_LEFT , DOWN_RIGHT , DOWN_LEFT , } public static ArrowVector CalculateVector(Vector2 start, Vector2 goal) { if(start.x == goal.x) { if(start.y > goal.y) { return ArrowVector.UP; } else { return ArrowVector.DOWN; } } if(start.y == goal.y) { if(start.x > goal.x) { return ArrowVector.LEFT; } else { return ArrowVector.RIGHT; } } if (start.x > goal.x && start.y > goal.y) return ArrowVector.UP_LEFT; if (start.x < goal.x && start.y > goal.y) return ArrowVector.UP_RIGHT; if (start.x < goal.x && start.y < goal.y) return ArrowVector.DOWN_RIGHT; if (start.x > goal.x && start.y < goal.y) return ArrowVector.DOWN_LEFT; return ArrowVector.NULL; }進む方向を決める
可読性を求めていたらいつの間にかハードコーディングしてました。
//////////////////////////////////////////////////////////////////////// // 4方向から進むに最適なタイルを算出 //////////////////////////////////////////////////////////////////////// private static TileBlock GetBestTile(TileBlock origin, int cost) { if (origin.GetTileType() == TileType.GoalTile || origin.GetAnalyzed()) { return origin; } var goalCoord = GetTileBlockByTileType(TileType.GoalTile).GetCoordinate(); var originCoord = origin.GetCoordinate(); var up = GetTileBlock(new Vector2(originCoord.x, originCoord.y - 1)); var bottom = GetTileBlock(new Vector2(originCoord.x, originCoord.y + 1)); var right = GetTileBlock(new Vector2(originCoord.x + 1, originCoord.y)); var left = GetTileBlock(new Vector2(originCoord.x - 1, originCoord.y)); //ふるいにかける if (up != null && up. GetTileType() != TileType.Walkable && up.GetTileType() != TileType.GoalTile) up = null; if (bottom != null && bottom. GetTileType() != TileType.Walkable && bottom.GetTileType() != TileType.GoalTile) bottom = null; if (right != null && right. GetTileType() != TileType.Walkable && right.GetTileType() != TileType.GoalTile) right = null; if (left != null && left. GetTileType() != TileType.Walkable && left.GetTileType() != TileType.GoalTile) left = null; //どれかが前のoriginだったらやめる if (up != null && up. GetAnalyzed()) up = null; if (bottom != null && bottom. GetAnalyzed()) bottom = null; if (right != null && right. GetAnalyzed()) right = null; if (left != null && left. GetAnalyzed()) left = null; //どれかがゴールだったらそこまで線を描画 if (up != null && up. GetTileType() == TileType.GoalTile) DrawLineCenterTileToTile(origin.GetCoordinate(), up. GetCoordinate()); if (bottom != null && bottom. GetTileType() == TileType.GoalTile) DrawLineCenterTileToTile(origin.GetCoordinate(), bottom.GetCoordinate()); if (right != null && right. GetTileType() == TileType.GoalTile) DrawLineCenterTileToTile(origin.GetCoordinate(), right. GetCoordinate()); if (left != null && left. GetTileType() == TileType.GoalTile) DrawLineCenterTileToTile(origin.GetCoordinate(), left. GetCoordinate()); var up_hcost = 0; var bottom_hcost = 0; var right_hcost = 0; var left_hcost = 0; //推定コストを計算 if (up != null) up_hcost = CalculateHeuristic(up. GetCoordinate(), goalCoord); if (bottom != null) bottom_hcost = CalculateHeuristic(bottom.GetCoordinate(), goalCoord); if (right != null) right_hcost = CalculateHeuristic(right. GetCoordinate(), goalCoord); if (left != null) left_hcost = CalculateHeuristic(left. GetCoordinate(), goalCoord); //データをセット if (up != null) up. SetAnalyzeData( cost, up_hcost ); if (bottom != null) bottom. SetAnalyzeData( cost, bottom_hcost ); if (right != null) right. SetAnalyzeData( cost, right_hcost ); if (left != null) left. SetAnalyzeData( cost, left_hcost ); var up_score = 0; var bottom_score = 0; var right_score = 0; var left_score = 0; if (up != null) up_score = up. GetScore(); if (bottom != null) bottom_score = bottom. GetScore(); if (right != null) right_score = right. GetScore(); if (left != null) left_score = left. GetScore(); var scores = new int[4] ; scores[0] = up_score ; scores[1] = bottom_score ; scores[2] = right_score ; scores[3] = left_score ; var hcosts = new int[4] ; hcosts[0] = up_hcost ; hcosts[1] = bottom_hcost ; hcosts[2] = right_hcost ; hcosts[3] = left_hcost ; var tiles = new TileBlock[4] ; tiles[0] = up ; tiles[1] = bottom ; tiles[2] = right ; tiles[3] = left ; var min_score = int.MaxValue ; var min_cost = int.MaxValue ; var min_hcost = int.MaxValue ; var min_tile = origin ; //一番スコアの低いものを探す for(int m = 0; m < 4; m++) { if (scores[m] == 0) continue; if (scores[m] > min_score) continue; if (scores[m] == min_score && cost >= min_cost) continue; min_score = scores[m]; min_cost = cost; min_tile = tiles[m]; min_hcost = hcosts[m]; } //自身をClose if (origin != null) { origin.Close(); } if (min_tile.GetTileType() == TileType.Walkable) { origin.SetAnalyzed(true); DrawLineCenterTileToTile(origin.GetCoordinate(), min_tile.GetCoordinate()); } return min_tile; } //////////////////////////////////////////////////////////////////////// // マップを探索 //////////////////////////////////////////////////////////////////////// public static async void AnalyzeMap() { LoggerForm.WriteSuccess("探索開始"); //各タイル座標に属性を付与 SetTileAttributesToAll(); var startTile = GetTileBlockByTileType(TileType.StartTile); var goalTile = GetTileBlockByTileType(TileType.GoalTile ); if(startTile == null || goalTile == null) { if(startTile == null) { LoggerForm.WriteError("Start tile not found."); } else if(goalTile == null) { LoggerForm.WriteError("Goal tile not found."); } return; } var startTileCoord = startTile.GetCoordinate(); var goalTileCoord = goalTile. GetCoordinate(); var k = 0; TileBlock bestTile = startTile; while (k < 999) { k++; if (bestTile == null || !IsTileBlockExists(bestTile.GetCoordinate())) { LoggerForm.WriteError("Tile not found."); break; } else if (bestTile.GetTileType() == TileType.GoalTile) { LoggerForm.WriteSuccess("Goal found."); break; } else { bestTile = GetBestTile(bestTile, k); } await Task.Delay(10); } }アルゴリズムについて
ご覧の通り、最短ルートは保証されてません。アルゴリズム(笑)です。
なぜこうなっているかというと、
進んでいる彼は左右上下1ブロックとスコアしか見えてません。
したがって、より良いスコアである右に進んでしまうのです。
しかしながら、全体で見れば左に進むのが明らかに最短ルートです。
こういった場合、左に進んでゴールに辿り着いた場合と
右に進んでゴールに辿り着いた場合の総タイル数を比較してルートを定める必要があります。
また、直線移動しかしません。行き止まりにも対応してません。「最短コースは保証されていないが、最低限の分析でゴールに辿り着ける」
という側面で見れば、ダイクストラ法とAスター法のいいとこ取りだと思います。(言い訳)
また改めて時間があれば、最適化します。といった感じで、中途半端ではありますが経路探査アルゴリズムを作ってみました。
ソースコードはGithubにあります。
- 投稿日:2019-12-13T16:20:22+09:00
"Task.Run"でビルドエラー「'System.Threading.Tasks.Task' に 'Run' の定義がありません。」
いくらググっても出てこない。
誰もそんなことでは悩んでいないらしい。試行錯誤した結果、
.NET Framework 4.5以上でなければTaskオブジェクトにRunメソッドは無かった。非同期処理やスレッド処理について解説したページで
Task.Runを使ったサンプルソースがあるけれど、
必須条件までは書かれていなかったなぁ。というより、
特別な理由がなければVisualStudioのプロパティの初期値を変更しないか。
- 投稿日:2019-12-13T15:40:09+09:00
【Unity, C#】続・privateな型やメンバにアクセスするには、多分これが一番早いと思います
この記事は【unityプロ技②】 Advent Calendar 2019の25日目の記事です。
この記事は【Unity, C#】internalな型やメンバにアクセスするには、多分これが一番早いと思いますの続編です。先にこちらをご覧ください。
この記事におけるソースコードは、全てPublic Domain
です。TL;DR
- C#の属性
IgnoresAccessChecksToAttribute
は実は任意の外部アセンブリのinternal/privateメンバに対して自由にアクセスできるヤバいやつだったよ。C#宇宙の 法則が 乱れる!- 自作のRoslynコンパイラをビルドパイプラインに乗っけることで、ワークフローを意識することなく運用できるよ。
- C#8の機能を使うことができるよ。
- くれぐれも じこせきにんで おねがいします。
IgnoresAccessChecksToAttribute
おさらい前回、こんな風に述べてました。
- InternalsVisibleToAttributeとは逆の方向に作用する。つまり、ライブラリ利用者側に設定することで、ライブラリに対するinternalアクセスを許可する
- フルネームはSystem.Runtime.CompilerServices.IgnoresAccessChecksToAttribute
- Base Class Libraryに載っていないが、ランタイム(CLR/CoreCLR)で作用する
- csc.exeやMsBuildを使わずに自力でコンパイルする際に、CSharpCompilationOptions.TopLevelBinderFlagsに対して特定のフラグを立てると有効になる
ところが、その後の調査により、privateな型やメンバに対してもアクセスができることが判明しました。
ちなみに、元記事にもキッチリ書いてありました。
- In other words, how to get access to internal and private members without needing to use reflection or something like InternalsVisibleToAttribute.
うっかりが過ぎる。
privateな型やメンバにアクセスするモチベーション
internalな型やメンバにアクセスするモチベーションと同じです。
internal要素以上に、privateアクセス修飾子によって隠蔽されているメンバは多く、その中には有用なものもあります。また、例えば拡張メソッドを実装する場面において、そのクラスのprivateメンバを扱うことができると考えるとどうでしょうか。
パフォーマンス/機能面で強力なハックを提供できるかもしれません。実際やろうとすると手順が面倒なことに気づく
前回のパッケージを使ってprivateアクセスしてみましょう。
- privateアクセスするコードを書く。IDE上でエラー表示になる(正常)
- Unity側ではコンパイルエラーになる(正常)
Assets/Open C# Project
でC#プロジェクトを生成AssemblyDefinitionFile
をDefine Constraints
で無効化し、コンパイルエラーを解消AssemblyDefinitionFile
を右クリックし、InternalAccessibleCompiler/Compile
でコンパイル- 生成したdllをインポート
わーい、とっても面倒くさい。
いちいち手作業でコンパイルするのもナンセンスですね。
こんなことならリフレクション使ったほうが早いんじゃね。ちなみに、C#プロジェクトを生成しても、対応する
AssemblyDefinitionFile
が無効化されるとC#ソリューションから外れ、インテリセンスがご臨の終です。どうもありがとうございました。
新しくIDEのインスタンス立ててC#プロジェクト読み込めばなんとかなりますが、なんか釈然としません...なんとか良い感じに運用してみましょう。
【方法1】 アセットの変更に対するコールバックで対処する
素直に実装すればこうでしょうか。
- 無効化されている
AssemblyDefinitionFile
から(なんとかして)C#プロジェクトを生成するAssetPostprocessor
で対象のcsファイルの変更を検知して、ファイルや外部参照、シンボルをコンパイラに引き渡すAssetPostprocessor.OnGeneratedSlnSolution
メソッドでソリューションファイルの変更を検知し、C#プロジェクトが除外されないように対応する運用するだけであればこの方法で十分そうですが、問題もあります。
- 無効化された
AssemblyDefinitionFile
が放置されているの- 外部参照解決がプラットフォーム的に正しいか確認が必要
- Unityのバージョンアップで参照が増えたりするので対応が必要
さらっと流しましたが
OnGeneratedSlnSolution
は文書化されていないメソッドです。
その他、以下のようにソリューションやプロジェクト生成時コールバックが用意されています。
これらはstaticメソッドであることに注意してください。using UnityEditor; using UnityEngine; namespace CSharpProjectSolutions { public class CustomAssetPostprocessor : AssetPostprocessor { // Unity標準のジェネレータ「以外」でC#プロジェクトを生成するかどうか返すコールバック(UnityVS等で利用). static bool OnPreGeneratingCSProjectFiles() { Debug.LogFormat("<color=cyan>OnPreGeneratingCSProjectFiles</color>"); return false; } // C#プロジェクトファイルが生成された後に、修正を適用するコールバック. static string OnGeneratedCSProject(string path, string content) { Debug.LogFormat("<color=blue>OnGeneratedCSProject:</color> {0}\n\n{1}", path, content); return content; } // C#ソリューションファイルが生成された後に、修正を適用するコールバック. static string OnGeneratedSlnSolution(string path, string content) { Debug.LogFormat("<color=orange>OnGeneratedSlnSolution:</color> {0}\n\n{1}", path, content); return content; } // VisualStudioのバージョンアップによってcsprojがUnityと互換性が無くなったときの「セーフガード」. // 後処理でcsprojを修正、または作り直す. 願わくば、これが必要になりませんように. // ...とソースコードに書いてあった(意訳) static void OnGeneratedCSProjectFiles() { Debug.Log("<color=red>OnGeneratedCSProjectFiles:</color>"); } } }【方法2】 自作コンパイラをビルドパイプラインに組み込む
そもそも、こんなに手間が掛かるのは、コンパイラへの入力としてC#プロジェクトファイルを使っているからです。
では、UnityにおいてC#プロジェクトファイルってビルドに必要なんでしょうか?答えはノーです。プロジェクトディレクトリ内からソリューションファイルやプロジェクトファイルを全て削除したとしても、コンパイルは元気に走ります。
コンパイラへの入力は、実際には何が使われているんでしょうか?以下、Unityにおけるコンパイルのに関する話になりますが、長くなりますので興味ない方は「めんどくさいのでパッケージを使う」まで読み飛ばし推奨。
Unity組み込みコンパイラの仕組み
UnityのC#コンパイラは
csc
で、unity_csc.bat
またはunity_csc.sh
としてプラットフォームごとのスクリプトにラップされています。
実際のところ、この辺の処理はUnityのバージョンによって異なりますが詳しくは割愛。
Unity 2019.3では、unity_csc.*
に関する記述はMicrosoftCSharpCompiler.StartCompilerにあります。protected override Program StartCompiler() { // プラットフォームに合ったコンパイラ(unity_csc)を探す var csc = Paths.Combine(EditorApplication.applicationContentsPath, "Tools", "RoslynScripts", "unity_csc"); if (Application.platform == RuntimePlatform.WindowsEditor) { csc += ".bat"; } else { csc += ".sh"; } csc = Paths.UnifyDirectorySeparator(csc); // コンパイラが見つからなかったら例外 if (!File.Exists(csc)) ThrowCompilerNotFoundException(csc); // responseファイルを生成する if (assembly.GeneratedResponseFile == null) { assembly.GeneratedResponseFile = GenerateResponseFile(assembly, options, tempOutputDirectory); } // ProcessStartInfoを生成し、新しいコンパイルプロセスを開始 var psi = new ProcessStartInfo() { Arguments = "/noconfig @" + assembly.GeneratedResponseFile, FileName = csc, CreateNoWindow = true }; var program = new Program(psi); program.Start(); return program; }自作コンパイラをビルドパイプラインに載せるには、この部分がハックできれば良さそうです。
どうやってハックできるのか確認していきましょう。まず、
MicrosoftCSharpCompiler
はCSharpLanguage.CreateCompiler
メソッドから参照されています。public override ScriptCompilerBase CreateCompiler(ScriptAssembly scriptAssembly, EditorScriptCompilationOptions options, string tempOutputDirectory) { return new MicrosoftCSharpCompiler(scriptAssembly, options, tempOutputDirectory); }そして、
CSharpLanguage
はScriptCompilers
のコンストラクタから参照されています。static ScriptCompilers() { SupportedLanguages = new List<SupportedLanguage>(); var types = new List<Type>(); types.Add(typeof(CSharpLanguage)); // typesにはCSharpLanguageしか入っていないので、以下と同じ // SupportedLanguages.Add(new CSharpLanguage()); foreach (var t in types) { SupportedLanguages.Add((SupportedLanguage)Activator.CreateInstance(t)); } // SupportedLanguagesにはCSharpLanguageしか入っていないので以下略 CSharpSupportedLanguage = SupportedLanguages.Single(l => l.GetType() == typeof(CSharpLanguage)); }staticコンストラクタに行き着きました。
CSharpSupportedLanguage
がこのタイミングで生成されていることがわかりますね。
SupportedLanguage
がリストとして受けられるようになっているのは、C#以外の言語(懐かしのUnityScript、Boo)に対応していた名残でしょう。ここから先の参照は本題とズレるので省きますが、この
CSharpSupportedLanguage
をどうにかして書き換えることができれば良さそうです。自作コンパイルを使ってコンパイルする
IncrementalCompiler
というパッケージを使ったことはありますか?
「ビルド時間が大幅に短縮できる」「C#7.2の機能が使える」という、Unity 2018.1〜2018.2向けの非常に強力なエディタ拡張パッケージで、実はUnity 2018.3以降では類似機能がビルトインされています。「自作コンパイラをビルドパイプラインに載せる」という意味では、
IncrementalCompiler
も同じことを行なっているはずです。
試しにパッケージを調べてみると、ScriptCompilers.CSharpSupportedLanguage
とScriptCompilers.SupportedLanguages
の上書きがキーになっているようでした(調べ方については割愛)。さっそく、それらを上書きしてみましょう。
なお、以下のコードはinternalアクセスを多用しているため、
Unity.InternalAPIEditorBridgeDev.001
等UnityEditor
にinternalアクセスが許可されているアセンブリ名を持つAssemblyDefinitionFile
が必要です。まずはエントリポイントからです。
InitializeOnLoad
属性を使って、ロード時に自動的に実行されるようにしましょう。
ここではScriptCompilers
のフィールドを書き換えるコードを用意します。[InitializeOnLoad] internal class CustomCSharpInstaller { static CustomCSharpInstaller() { var customLanguage = new CustomCSharpLanguage(); // SupportedLanguagesにカスタムC#を追加. ScriptCompilers.SupportedLanguages.RemoveAll(x => x.GetType() == typeof(CustomCSharpLanguage)); ScriptCompilers.SupportedLanguages.Insert(0, customLanguage); // CSharpSupportedLanguageはreadonlyなのでリフレクションで上書き. typeof(ScriptCompilers) .GetField("CSharpSupportedLanguage", BindingFlags.Static | BindingFlags.NonPublic) .SetValue(null, customLanguage); // こちらも上書き. EditorBuildRules.GetPredefinedTargetAssemblies() .Where(x => x != null && x.Language != null) .First(x => x.Language.GetType() == typeof(CSharpLanguage)) .Language = customLanguage; } }これにより、C#のコンパイル時に、
CSharpLanguage
の代わりにCustomCSharpLanguage
が選択されるようになりました。
次に、
CustomCSharpLanguage
を実装していきますが、こちらはCSharpLanguage
(ソース)を継承すれば最小限のコードで済みます。internal class CustomCSharpLanguage : CSharpLanguage { public override ScriptCompilerBase CreateCompiler(ScriptAssembly scriptAssembly, MonoIsland island, bool buildingForEditor, BuildTarget targetPlatform, bool runUpdater) { // カスタムコンパイラを使うかどうかのフラグ. // ScriptAssemblyやMonoIslandにはファイル一覧、参照一覧、シンボル一覧、出力ファイル名などの情報が格納されている. // それに応じて必要なアセンブリのみコンパイラを切り替えることが可能。 bool useCustomCompiler = true; if(useCustomCompiler) // カスタムコンパイラを使う. return new CustomCSharpCompiler(island, runUpdater); else // 使わない場合はデフォルトのコンパイラを使う. return base.CreateCompiler(scriptAssembly, island, buildingForEditor, targetPlatform, runUpdater); } }このように、
CSharpLanguage
は動的にコンパイラクラスのインスタンスを返せます。
Unity2019.2までは、この部分でmsc/cscの切り替えを行なっていました。
なお、Unity2019.3からはcsc(MicrosoftCSharpCompiler
)のみです。
最後に、コンパイラクラスを作成しましょう。
このクラスはresponse file(コンパイルオプションを記述したファイル)を生成し、それを入力としてコンパイラプロセスを起動することが責務です。
MicrosoftCSharpCompiler
(ソース)を継承すればこちらも簡単です。internal class CustomCSharpCompiler : MicrosoftCSharpCompiler { public CustomCSharpCompiler(MonoIsland island, bool runUpdater) : base(island, runUpdater) { } protected override Program StartCompiler() { // 継承元のコンパイルプロセスは即終了させる. var p = base.StartCompiler(); p.Kill(); // 最後に生成されたresponse fileを取得する. // 複数のファイルが生成される場合があるので、outオプションで判定する. var outopt = string.Format("/out:\"{0}\"", m_Island._output); var responsefile = Directory.GetFiles("Temp", "UnityTempFile*") .OrderByDescending(f => File.GetLastWriteTime(f)) .First(path => File.ReadAllLines(path).Any(line => line.Contains(outopt))); // 自作のコンパイラでresponse fileを処理する. var psi = new ProcessStartInfo() { Arguments = ..., FileName = ..., CreateNoWindow = true }; // プロセスを開始する. var program = new Program(psi); program.Start(); return program; } }継承元(
MicrosoftCSharpCompiler
)でcscを使ったコンパイルプロセスを作っているので、継承元のものは終了させましょう。
response fileは、継承元のコンパイラクラスで作成され、
Temp
フォルダに保存されるので、最新のものをピックアップしましょう。
response fileの生成は継承元のコンパイラクラスに任せましょう、簡単に外部参照やシンボル等の整合性が取れます。
ちなみに、responsefileの中身はこんな感じです。
C#プロジェクトの内容をコンパイルオプションに置き換えたようなイメージですね。/target:library /nowarn:0169 /out:"Temp/*********.dll" /debug:portable /optimize- /nostdlib+ /preferreduilang:en-US /langversion:latest /reference:"/********/Unity.app/Contents/Managed/UnityEngine/UnityEngine.dll" /reference:"/********/Unity.app/Contents/Managed/UnityEngine/UnityEngine.AIModule.dll" /reference:"/********/Unity.app/Contents/Managed/UnityEngine/UnityEngine.ARModule.dll" /reference:"/********/Unity.app/Contents/Managed/UnityEngine/UnityEngine.AccessibilityModule.dll" /reference:"/********/Unity.app/Contents/Managed/UnityEngine/UnityEngine.AnimationModule.dll" /reference:"/********/Unity.app/Contents/Managed/UnityEngine/UnityEngine.AssetBundleModule.dll" ...以下、シンボル定義とcsファイルの一覧余談ですが、なんでわざわざresponse fileを作る必要があるんでしょうか?
正解はProcess.Startで引数が長すぎて死ぬからです。
あと、文字列がダブルクォーテーションで囲まれてるのも注意が必要です。
私はどちらの罠も踏み抜きました。
長くなりましたが、これがUnity上で自作C#コンパイラを動かすための雛形となるコードです。
このコードを好きなように改造し、dllにコンパイルしてインポートすることで、csファイルのコンパイルが始まる前に自作C#コンパイラをビルドパイプラインに載せられます。前回の
IgnoresAccessChecksTo
の下準備よりもめんどくさいですね。めんどくさいのでパッケージを使う&デモ
今回は、先述のコンパイラを同梱済みのUnity向けに公開しているパッケージをデモとして使います。
こちらからデモプロジェクト一式をダウンロードできます。
- 動作にはdotnet 3.0以上が必要です
- コマンドプロンプト(Windows)やターミナル(Mac)で、
dotnet --version
を実行したときに、3.0.x
以上が表示されていればインストール不要です- https://dotnet.microsoft.com/download からインストールしてください
- UnityプロジェクトをUnityエディタで開きます
Coffee.OpenSesame.Test.cs
がアクセシビリティに関するコンパイルエラー(CS0122)を吐いてますね。安心してください、privateアクセスしているだけです。あなたのUnityは正常ですよ。
- プロジェクトビューで
Tests/Coffee.OpenSesame.Test.asmdef
を選択し、コンテキストメニュー(右クリック)からOpenSesame Compiler > Setting
をクリックし、開いたウィンドウで以下のように入力します
![]()
- Open Sesame Compiler: チェックを入れる
- Publish Folder: Assets/Editor (初期値)
Save
を押すと、内容が保存されて、コンパイルが実行されます。そのまましばらく待つと...コンパイルエラーが消えました!
![]()
- ツールバーの
Window > General > Test Runner
を選択し、テストランナーウィンドウを開き、Run All
を押すと...privateアクセステストが通りました!
![]()
- この後は
Tests/Coffee.OpenSesame.Test.cs
にinternal/privateアクセスを追加しても、コンパイルエラーが吐かれませんよ!- 次に、プロジェクトビューで
Tests/Coffee.OpenSesame.Test.asmdef
を選択し、コンテキストメニュー(右クリック)からOpenSesame Compiler > Publish
を選択しましょう。- dllファイルが生成されました。このdllは、もはやコンパイラの手を借りることなくinternal/privateアクセスが可能な存在です。
![]()
- このように、ポータブルなdllファイルを生成することで、dllをインターフェースとしたinternal/privateアクセスが実現できます。もちろん、別プロジェクトでも利用できますし、パッケージとして配布することも可能です。
このパッケージ(com.coffee.open-sesame-compiler)のアピールポイント
- ワークフローを変化させずに、internalアクセスもprivateアクセスもできちゃう
- C#宇宙の 法則が 乱れる!
- インストールするだけで使え、覚えるべきことが少ない
- 必要最低限のアセンブリだけを自作コンパイルで処理できる
- ↑の設定画面で
Open Sesame Compiler
にチェックを入れたアセンブリのみ処理できる- それ以外はデフォルトのコンパイラで処理するので、影響範囲が小さい
AssemblyDefinitionFile
を無効化しなくていい
- internal/privateな要素を使っていても、コンパイルエラーにならない
- 間違った使い方によるエラーは報告してくれるので安心
- Publish機能を使えば、ポータブルなdllとしてエクスポートできる
- 配布する際にコンパイル部分の依存が不要になる
- C#8が使える
- .Net 3.5でも.Net 4.xでも動く
気になるところ
機能検証に時間を取りすぎた結果、テストにあまり時間が取れませんでした...
「とりあえず、こういうことができる」事が分かったという段階ですね。今後に期待してください。
IgnoresAccessChecksToAttribute
によるinternal/privateアクセスでできないことは?
- internal/privateクラス・インターフェースの継承
- internalクラス・インターフェースの継承は
InternalsVisibleTo
を併用することで可能- privateクラスに対する拡張メソッド
- 拡張メソッドの仕様上仕方ない気がするけど
- たぶん、まだあると思うので見つけたらコメントください!
- ランタイムでも動くの?
- 未確認です...
- IL2CPP対応してる?
- 未確認です...
- ブレークポイントは?
- 未確認です...
- サポートしてるバージョンは?
- Unity 2018.3〜2019.2までは確認しました(Mac)
- Unity 2019.3と2020.1で大幅に変更があったようなので、今後対応します
- なんかエラー出るんだけど?
- エディタを一度閉じた後、
Library/ScriptAssemblies
を削除して再起動してください- IDEだとprivate要素にエラー出たまんまなんだけど?
それはいわゆる、コラテラルダメージというものに過ぎない。目的の為の、致し方ない犠牲だ。終わりに
internal/privateアクセスが手軽にできるようになりました。
正直、リフレクション憎しのエネルギーで結構ヤバいものを生み出してしまった感があります。
くれぐれも じこせきにんで おねがいします。そして、この記事を書いている間に「あれ?
UnityEditor.Modules.ICompilationExtension
使ったらもっと簡単にイケるんじゃね?」と気づきました。
そういうことに気を取られるから遅筆なんだぞ!
後でその検証もやります。
【追記】ダメでした。この場を借りて、様々な情報をご提供頂きました@pCYSl5EDgoさんにお礼を申し上げますm(_ _)m
- 投稿日:2019-12-13T12:53:43+09:00
C# 非同期
非同期というのは並列処理の一種。
例えば、コンビニで例えると店員二人で客の対応を行うのがマルチスレッド。
対して、お弁当温めてくださいと言われたときに電子レンジに温めるのを任せ、他の客の対応を行うのが非同期。コードで例を見てみよう。
その前に準備準備:Stopwatchクラス
C#にはデフォルトで時間を測るStopwatchクラスが存在する。
stopWatch.Startが時間計測スタート。Program.csusing Newtonsoft.Json; using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; using System; using System.Diagnostics; using System.Threading; namespace ConsoleApp9 { class Program { static void Main() { var stopWatch = new Stopwatch(); stopWatch.Start(); Thread.Sleep(3000); Console.WriteLine(stopWatch.Elapsed); } } }実行結果
00:00:03.0010707async await
メソッドにasyncをつけるとそのメソッドは非同期メソッドになる。
awaitをつけると非同期メソッドが終了するまで待機する。
Task.Delay(1)はダミー行。asyncメソッドの中でawaitがないと同期で実行されてしまうため入れている。
戻り値のTaskというのはjavascriptでいうところのresolve。
Taskは非同期メソッドの状態を返す。Program.csusing System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; namespace ConsoleApp9 { class Program { static void Main() { var stopwatch = new Stopwatch(); stopwatch.Start(); Exe1(); Console.WriteLine(stopwatch.Elapsed); } async static Task Exe1() { await Task.Delay(1); Thread.Sleep(3000); } } }実行結果
00:00:00.0495786これでawait Task.Delay(1)の行がなければ同期実行されるので3秒待つことになる。
Program.csusing System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; namespace ConsoleApp9 { class Program { static void Main() { var stopwatch = new Stopwatch(); stopwatch.Start(); Exe1(); Console.WriteLine(stopwatch.Elapsed); } async static Task Exe1() { Thread.Sleep(3000); } } }実行結果
00:00:03.0207121awaitをつけるとasyncメソッドが終わるまで待つ
Program.csusing System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; namespace ConsoleApp9 { class Program { async static Task Main() { var stopwatch = new Stopwatch(); stopwatch.Start(); await Exe1(); Console.WriteLine(stopwatch.Elapsed); } async static Task Exe1() { await Task.Delay(1); Thread.Sleep(3000); } } }実行結果
00:00:03.0207121後で待機することも可能。
Program.csusing System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; namespace ConsoleApp9 { class Program { async static Task Main() { var stopwatch = new Stopwatch(); stopwatch.Start(); var task = Exe1(); Console.WriteLine("Hello"); await task; Console.WriteLine(stopwatch.Elapsed); } async static Task Exe1() { await Task.Delay(1); Thread.Sleep(3000); Console.WriteLine("Hello2"); } } }実行結果
Hello Hello2 00:00:03.0551079複数のTaskを待機する。
Thread.SleepとTask.Delayはどちらも待機なのでTask.Delayを使っていく。
Task.WhenAll(list)はlist内のtaskが全て終了するまで待機する。Program.csusing System; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; namespace ConsoleApp9 { class Program { async static Task Main() { var stopwatch = new Stopwatch(); stopwatch.Start(); var task = Exe1(); var list = new List<Task>(); for(var i = 0; i < 5; i++) { list.Add(Exe1()); } await Task.WhenAll(list); Console.WriteLine("Hello"); Console.WriteLine(stopwatch.Elapsed); } async static Task Exe1() { await Task.Delay(3000); Console.WriteLine("Hello2"); } } }WPFでの非同期
WPFのイベントで非同期メソッドを使うことによって、UIを妨害することなくイベントの実行ができる
まずは非同期を使わない場合MainWindow.xaml<Window x:Class="WpfApp13.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WpfApp13" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <Button Content="ボタン" Width="100" Height="100" Click="Button_Click"></Button> </Grid> </Window>MainWindow.xaml.csusing System.Threading; using System.Windows; namespace WpfApp13 { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { Thread.Sleep(3000); MessageBox.Show("Buttonをクリックしました。"); } } }実行すると、3秒経った後メッセージボックスが表示される、3秒の間はWPFを操作することはできない。
このように重い処理がイベントに入ってしまっている場合ユーザーの利用を阻害してしまうことがあるので、その場合非同期を使うのが有効となる。非同期を使った場合。
MainWindow.xaml.csusing System.Threading.Tasks; using System.Windows; namespace WpfApp13 { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private async void Button_Click(object sender, RoutedEventArgs e) { await Task.Delay(3000); MessageBox.Show("Buttonをクリックしました。"); } } }この場合メッセージを出すのはボタンをクリックしてから3秒後になるが、その間操作はできる。
戻り値がTaskではなくvoidになっているのはメソッドの登録を行うときにvoidでないといけないから。Waitメソッドによる待機
実はawaitを使わなくても待機する方法がある。
Program.csusing System; using System.Diagnostics; using System.Threading.Tasks; namespace ConsoleApp9 { class Program { static void Main() { var stopwatch = new Stopwatch(); stopwatch.Start(); Exe1().Wait(); Console.WriteLine(stopwatch.Elapsed); } async static Task Exe1() { await Task.Delay(3000); Console.WriteLine("Hello2"); } } }実行結果
Hello2 00:00:03.0553411しかし、このメソッドにはデメリットがありWPFなどで使うとデットロックが発生し動かなくなってしまう。
MainWindow.xaml.csusing System.Threading.Tasks; using System.Windows; namespace WpfApp13 { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { WaitWrapMethod().Wait(); MessageBox.Show("Buttonをクリックしました。"); } private async Task WaitWrapMethod() { await Task.Delay(1000); } } }実行して、Button_Clickイベントと紐づいているボタンをクリックすると、デットロックを起こし動かなくなる。
Task.Delay(1000).ConfigureAwait(false)というようにConfigureAwait(false)をしてあげるとデットロックは起こらない。
MainWindow.xaml.csusing System.Threading.Tasks; using System.Windows; namespace WpfApp13 { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { WaitWrapMethod().Wait(); MessageBox.Show("Buttonをクリックしました。"); } private async Task WaitWrapMethod() { await Task.Delay(1000).ConfigureAwait(false); } } }実行してボタンをクリックしてみるとデットロックは起こらない。
複数のTaskの待機
WaitAllメソッドを使うと複数のTaskをawaitなしで待機することができる。
Program.csusing System; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; namespace ConsoleApp9 { class Program { static void Main() { var stopwatch = new Stopwatch(); stopwatch.Start(); var list = new List<Task>(); for(var i = 0; i < 5; i++) { list.Add(Exe1()); } Task.WaitAll(list.ToArray()); Console.WriteLine(stopwatch.Elapsed); } async static Task Exe1() { await Task.Delay(3000); Console.WriteLine("Hello2"); } } }実行結果
Hello2 Hello2 Hello2 Hello2 Hello2 00:00:03.0633685非同期メソッドの戻り値
Genericを使うことによって戻り値を指定することができる。
例えば、stringが戻り値である場合非同期メソッドの時はTaskとすればよい。Program.csusing System; using System.Diagnostics; using System.Threading.Tasks; namespace ConsoleApp9 { class Program { async static Task Main() { var stopwatch = new Stopwatch(); stopwatch.Start(); var t = await Exe1(); Console.WriteLine(t); Console.WriteLine(stopwatch.Elapsed); } async static Task<string> Exe1() { await Task.Delay(3000); return "Hello"; } } }実行結果
Hello 00:00:03.0480318Resultによる非同期メソッドの戻り値
Task型の変数があったとき、Resultフィールドによって値を見ることができる。
Program.csusing System; using System.Diagnostics; using System.Threading.Tasks; namespace ConsoleApp9 { class Program { static void Main() { var stopwatch = new Stopwatch(); stopwatch.Start(); var t = Exe1(); var val = t.Result; Console.WriteLine(val); Console.WriteLine(stopwatch.Elapsed); } async static Task<string> Exe1() { await Task.Delay(3000); return "Hello"; } } }実行結果
Hello 00:00:03.0586377
- 投稿日:2019-12-13T12:23:54+09:00
Codecademy 何?
初めに
この記事は、SLP KBIT Advent Calendar 2019 の13日目の記事です。
プログラミングを自分でやってみたいときこのウェブサイトは役に立つかもしれない。
どうして Codecademy❓
良い点
○ たくさん言語を学ぶことができる
○ 説明が詳しい
○ シラバスに沿って勉強する
悪い点
○ 日本語に翻訳しなければならない
○ 無料ではないやってみよう
1.言語を選択する
2.説明を読む
3.実際にやってみよう
4.問題あったら教えてくれる
最後に
― 色々な言語やってみたぜひやってください
― わかりやすく勉強できる
ー お金がかかるだけどとても役に立つ参考
codecademy.com
- 投稿日:2019-12-13T10:51:07+09:00
C# で学ぶ関数指向概念超入門(?) / 実際に輸入してみた実験の記録
↑ C# Advent Calendar 2019 13 日目です ↑
この記事は、
- プログラミング自体が興味の対象な人
- プログラミング言語の穴を見つけては楽しくなるタイプの人 (いるのか?)
- 関数指向プログラミングにそこそこ馴染みある人
みたいな人を対象にしています (誰得)。
C# のいろんなネタ探しを兼ねて、長らく「過去から未来まで、コード中に書かれるであろう全ての正規表現を一切滅ぼすことを使命とした代替ライブラリ」を書いていたのですが、気がついたら関数指向プログラミング的な仕掛けが (意図せず) あれこれ混入していたので、そこらへんを適当にピックアップしつつ、いくつか馴染みがなさそうな概念について解説してみます。
とか言って、ある程度関数指向を知ってる人向けです。型クラス的なインターフェース
インターフェースは型クラスのようなものとはよく言われますが、まあ外から差し込みができるできないとかの違いはあるけどだいたい似たようなものです!似たようなものとして使うことができます!
さっそく例ですが、
public enum Ordering { LT, EQ, GT } public interface IComparable<T> where T : IComparable<T> { Ordering CompareTo(T other); } TComparable Max<TComparable>(TComparable first, TComparable second) where TComparable : IComparable<TComparable> => first.CompareTo(second) switch { Ordering.LT => second, Ordering.GT => first, _ => first, // EQ };メソッドの引数/戻り値としてインターフェースを直接扱う代わりに、ジェネリック型の引数を取って型引数の制約にインターフェースを指定するように書き換えているだけですね。
このMax
メソッドの例では、単純にインターフェースを引数に取る場合とは異なり、first
とsecond
が「同じ型でなければならない」という重要な制約が課されています。単に同じインターフェースを実装しているだけでは、その実体が同じ型であるかどうかまでは分からないのですね。
また、戻り値の型を引数と同じ具象型にできているところもポイントです。ジェネリクスを利用しなかった場合、どうやってもIComparable<T>
のインターフェースを返すことまでしかできません。
値型でも参照型でも関係なく具体的な型に沿ってメソッドを生成できるので、ボクシングの回避にも使えます。ところで、
where TComparable : IComparable<TComparable>このような、型制約でインターフェースの型引数に「それ自身の型」を渡しているものを勝手に「自己言及型引数」と呼んでいますが、こうするとメソッドに渡すオブジェクトの型と
IComparable<TComparable>
のTComparable
が同じものでない限りコンパイルが通らなくなるので、IComparable<T>
の有効な実装を自己言及に制限することができます。
TComparable
の実体が仮にIComparable<X>
を実装していたとしても、TComparable
とX
が一致しないのでMax
に渡されることを防げるのです。
これはインターフェースを型クラス的に使うために必須ではないですが、こういうテクニックも有効活用できるよという話でした。最後にインターフェース定義の方の、
public interface IComparable<T> where T : IComparable<T>この
T
に対する制約は実際のところ必要ではないのですが、実装者へのアノテーションとして定義してあると親切かと思って入れています。運用上ではこれ以外の定義は許容されないわけなので。RankNTypes
C# でもインターフェースを悪用すればランク2型のようなものを再現することができます。
myfunc :: forall a. a -> (forall x. Show x => x -> a -> String) -> String例えばこんな (適当な) 関数を定義したいと思います。
C# では、こうです。public string MyFunc<A>(A a, ISomeFunc<A> f) => ...; public interface ISomeFunc<T> { string Invoke<X>(X x, T a) where X : IShow; }さすがにいろいろめんどくさいですが、まあ、なんかできました。ジェネリックなインターフェースの中に型制約付きなジェネリックメソッド定義を持たせることで、型が合います。
これデリゲートとラムダ式が Higher rank types を公式にサポートしてくれたらなーーーって思いますね!!!存在型
存在型は関数指向というよりはむしろオブジェクト指向の考えに近いので……。
public interface IX<T, T2> { } public class X<T, T2, T3, T4> : IX<T, T2> { }単純にインターフェースを介するときに型引数のいくつかを隠すことで共通化します。はい、普通ゥーのオブジェクト指向ですね!
インターフェースではなく抽象クラスでやるパターンもあります。
隠した型を使う処理は、シグネチャに表れないように普通にクラス内メソッド中に書けばいいだけです。不動点
みんなだいすき Fixed point
「関数
f
の不動点」とは、f(x) = x
となるx
のことです。
この不動点を「計算する」関数がfix
で、fix
にf
を渡すとx
が手に入ります。このfix
を不動点コンビネータと呼びます。
fix(f) = f(fix(f))
で定義され、fix(f) = x
です。C# でもちょっと変形してるけど定義そのままみたいなのが使えます。主に
Lazy<T>
とかラムダ式とかを使って作ります。// × 自然な定義だけどこれは無限再帰して動かない T Fix<T>(Func<T, T> f) => f(Fix(f)); // ○ 値を関数で包んで評価タイミングを調整可能にしたもの Func<T> Fix<T>(Func<Func<T>, Func<T>> f) => () => f(Fix(f))(); // ◎ さらにパラメータを任意の T に一般化した形 Func<T, TResult> Fix<T, TResult>(Func<Func<T, TResult>, Func<T, TResult>> f) => x => f(Fix(f))(x);正格評価な環境だと最初の定義では
Fix(f)
の評価が無限再帰して止まらないので、f
の代わりにx => f x
を使うなどして評価タイミングを遅らせます。ちなみに、「
x = f(x)
となるx
」という不動点の定義をそのまま書き下したような書き方もあって、効率の面でも有利であるため実際の Haskell ではfix f = let x = f x in xのような実装が採用されています。
C# でも、工夫すれば同じようなことできます!末尾呼び出し最適化
C# は、実は条件を満たすことで末尾呼び出しをジャンプに置き換えることができます。つまり、ちゃんと工夫すれば、無限な再帰的呼び出しを真っ当に実行できるワンダーなコードが書けます。
一応先に用語を整理しておくと、
- 末尾再帰
- 関数の評価手続きの最終ステップが「自身の呼び出し」であるもの
- 末尾再帰最適化
- 関数が「末尾再帰」になっているとき、関数定義自体を一つのループ構造にリライトするコンパイラ最適化
- 末尾呼び出し
- 関数の評価手続きの最終ステップが「何らかの関数の呼び出し」であるもの
- 末尾呼び出し最適化
- 関数の「末尾呼び出し」を、コールスタックを積まずにジャンプに置き換えるコンパイラ最適化
末尾再帰の概念は末尾呼び出しの特殊化されたパターンに過ぎないのですが、末尾再帰最適化は (たいてい) 中間言語レベル、末尾呼び出し最適化はアセンブリレベルでの最適化になるので、最適化の文脈では2つはやや毛色が違う扱いをされることが多いです。
末尾再帰最適化が効く言語はそこそこ数がありますが、末尾呼び出し最適化を行えるランタイムはそう多くなかったと記憶しています。そういう意味では、.NET のランタイムは結構ステキ。
ただし、肝心の末尾呼び出しが最適化される条件がやや厳しい (ここ最近の新機能でさらに厳しくなった) ので、アセンブリに手を加えて条件を緩和するハックも利用して実用レベルにしています。必要に駆られて作った絶対末尾呼び出し最適化したい人向けビルドツール
CPS 変換
CPS 変換というのは、一般的に、一連の手続きをぶつ切りにして小さい手続きのチェーンに変換し、再帰的に呼び出すような構造に変換することをいいます。
型の上では、値a
を関数forall r. (a -> r) -> r
に置き換えることに相当します。
例えばa -> b
で型付けされる関数を CPS 変換するとa -> (b -> r) -> r
になります。
ここでおなじみ
Factorial
くんに登場していただき、末尾呼び出しとそうでないもの、そして CPS 変換の定義を並べてみました。でもよくある内容だし長いので読まなくてもいいです。
末尾じゃない普通の定義
int Factorial(int x) => (x == 0) ? 1 : Factorial(x - 1) * x; Factorial(3) == <Factorial(3 - 1)> * 3 | Factorial(2) == <Factorial(2 - 1)> * 2 | | Factorial(1) == <Factorial(1 - 1)> * 1 | | | Factorial(0) == 1 | | (1) * 1 == 1 | (1) * 2 == 2 (2) * 3 == 6
末尾再帰
int Factorial(int x, int acc) => (x == 0) ? acc : Factorial(x - 1, acc * x); Factorial(3, 1) == <Factorial((3 - 1), (1 * 3))> | Factorial(2, 3) == <Factorial((2 - 1), (3 * 2))> | | Factorial(1, 6) == <Factorial((1 - 1), (6 * 1))> | | | Factorial(0, 6) == 6 | | 6 | 6 6 /* ↑ は ↓ とみなせる */ | Factorial(3, 1) == <Factorial((3 - 1), (1 * 3))> | Factorial(2, 3) == <Factorial((2 - 1), (3 * 2))> | Factorial(1, 6) == <Factorial((1 - 1), (6 * 1))> | Factorial(0, 6) == 6
CPS 変換による末尾再帰
int Factorial(int x, Func<int, int> cont) => (x == 0) ? cont(1) : Factorial(x - 1, acc => cont(acc * x)); Factorial(3, (x => x)) == <Factorial((3 - 1), (x => x * 3))> | Factorial(2, (x => x * 3)) == <Factorial((2 - 1), (x => (x * 2) * 3))> | | Factorial(1, (x => (x * 2) * 3)) == <Factorial((1 - 1), (x => ((x * 1) * 2) * 3))> | | | Factorial(0, (x => ((x * 1) * 2) * 3)) == <(((1 * 1) * 2) * 3)> | | | | (((1 * 1) * 2) * 3) == <((1 * 2) * 3)> | | | | | ((1 * 2) * 3) == <(2 * 3)> | | | | | | (2 * 3) == 6 | | | | | 6 | | | | 6 | | | 6 | | 6 | 6 6 /* ↑ は ↓ とみなせる */ | Factorial(3, (x => x)) == <Factorial((3 - 1), (x => x * 3)> | Factorial(2, (x => x * 3)) == <Factorial((2 - 1), (x => (x * 2) * 3))> | Factorial(1, (x => (x * 2) * 3)) == <Factorial((1 - 1), (x => ((x * 1) * 2) * 3))> | Factorial(0, (x => ((x * 1) * 2) * 3)) == <(((1 * 1) * 2) * 3)> | (((1 * 1) * 2) * 3) == <((1 * 2) * 3)> | ((1 * 2) * 3) == <(2 * 3)> | (2 * 3) == 6
通常の再帰関数は、自身を呼び出し終わった後に「何か戻り値を処理する手続き」が残るのに対して、末尾再帰とは、呼び出し終わった後の処理が「戻り値をそのまま返すしかやることがない状態」になっているものを指します。
ということは、この返すだけの処理は省略しても結果が変わらなくて、最後の計算結果を直接使えばいい気がしますね!これが「末尾再帰は機械的に最適化が可能 (できるだけで必ずするとは言っていない)」という意味です!ここで、CPS 変換とは、本来末尾再帰にならないような再帰関数を何でも末尾再帰に変換できてしまうマジックです。
再帰関数内の「何か戻り値を処理する手続き」自体を関数にしてしまえば、「続きの処理」をなくすことができます!
コード例を見比べてみると、CPS では通常の再帰定義と同等の処理がそのままフラットに展開されていることが分かります。
まあ、これは実際にやってみて慣れないと分からないやつなので、覚えたいなら実際に何か作ってみたほうが早いです。CPS 変換を利用して再帰的な呼び出しをなんでも末尾呼び出しにできるということは、C# でも任意の再帰的な処理を末尾呼び出し最適化の対象にできるということです!素晴らしいですね!
モナド
モナドが何なのかについては、以前書いた記事を見てもらった方が早いとして……。
C# でモナドを書くなら、ジェネリックなクラスA<T>
に対して「T
からA<T>
を作れる関数」と「A<T>
とT -> A<T2>
な関数の2つからA<T2>
が作れる関数」があれば、後はちょっとしたルールを満たせばモナドになります!簡単ですね!ちなみに、モナド的な性質を持っているなら、以下のような (拡張) メソッドを定義しておくと、Linq のクエリ構文を do 記法的に扱うことができます。
順次処理するような複数の計算を並べて書くときなどに、クエリ構文が使えると捗る場面がたまにあります。
A<T2> Select<T1, T2>(this A<T1>, Func<T1, T2>)
- これがないとはじまらない、クエリ構文を使うための必須メソッド。
- クエリ構文最終段の
select
や、let
キーワードを書いたときに使用される。A<T3> SelectMany<T1, T2, T3>(this A<T1>, Func<T1, A<T2>>, Func<T1, T2, T3>)
- モナド的に使うにはこれが必要。
- 多重 from が書けるようになる。
A<T1> Where<T1>(this A<T1>, Func<T1, bool>)
- do 記法でいう guard に相当。
- 定義できると where キーワードが使えるようになる。
他にも
GroupBy
やOrderBy
などクエリ構文を拡張できるメソッドはまだまだありますが、とりあえず主要なものだけ書きました。
あと、1引数のSelectMany
(==bind
) はクエリ構文のために定義する必要は無いです。おわり
はい。最初から最後まで、だから何?って感じの記事でしたね!
まあ、C# でもかなりアグレッシブな関数指向プログラミングが実際可能であることは分かりました。どれもうまいことオブジェクト指向の上に乗っけることができています。
ただ、本来的に言語仕様がそういう使い方を想定していないので、いざやろうとすると若干めんどくさい場面が多いです。
もう少し楽に書けるような言語仕様が増えてくれたらって思いますが、増える気配はないですね……きっと誰も使わないですし……。
でもでも、もっとカジュアルな感じに関数指向なら全然書けるので、みんなももっと Functional していってもいいと思います!最後に、今回例に出したプロジェクトでは他にもいろいろチャレンジングなコードが書かれていて、とても C# とは思えないようなコードが散りばめられた極北プロジェクトに仕上がってるので、外面からでも内面からでも、もし興味が沸いたなら是非見ていって下さい。ループ文がなくても、ライブラリはつくれる。
- 投稿日:2019-12-13T08:41:57+09:00
HoloLens2 Tutorial やってみた
HoloLens2
いよいよMicrosoftのMRデバイスHololens2日本解禁ですね!!
https://jp.techcrunch.com/2019/11/08/2019-11-07-microsofts-hololens-2-starts-shipping/近未来の象徴、夢のデバイスであるHoloLens2、自分が手に入るのかはいつか全くわかりませんが、
エミュレーターを使ってチュートリアルを実践して、その気になっておこうという記事になります。
アドベントカレンダーも折り返し、今年も残りあとわずか。希望をもっていきましょう!!参考にするチュートリアルはこちら
https://docs.microsoft.com/ja-jp/windows/mixed-reality/mrlearning-base基本的にSDKが違うので、HoloLens2用に作ったアプリはHoloLens1では動かないみたい。逆に1で作ったアプリも2では動かないのかな?
チュートリアルやってみよう
1. 環境準備(Hololens2開発はWindows10必須だよ!)
https://docs.microsoft.com/ja-jp/windows/mixed-reality/install-the-tools
上のリンクを参考に順番にソフトをインストールしていきます。(すべてyesマンでポチポチリンクを踏んでいきます。)
エンジンはUnityを選択。① VisualStudio 2019 (16.4.0にアップデート)
② Windows 10 SDK (10.0.18362.0)
③ HoloLens 2 エミュレーター (2019 年 11 月の更新プログラム)(インストールのリンク:10.0.18362.1039)
④ Unity 2018.4.13f1 (2018.4.xが推奨らしいのでその中で一番新しいものを)
※UWP関連の物に✔(画像はvuforia等今回は不要なものもチェックしている)
⑤MRツールキット (とりあえず全部ダウンロード)
- Microsoft.MixedReality.Toolkit.Unity.Examples.2.1.0.unitypackage 55.5 MB
- Microsoft.MixedReality.Toolkit.Unity.Extensions.2.1.0.unitypackage 9.68 KB
- Microsoft.MixedReality.Toolkit.Unity.Foundation.2.1.0.unitypackage 8.85 MB
- Microsoft.MixedReality.Toolkit.Unity.Tools.2.1.0.unitypackage 79.8 KB
2. プロジェクトと最初のアプリケーションの初期化
インストールしたunityを起動してNewをクリック
適当に名前を付けてcreate project
MR Tool Kitをインポート
これもチュートリアルに従って・・・ダウンロードしたツールキットのパッケージをすべてインポートする。
するとさっきまでメニューバーにいなかったMixed Reality toolkitが満を持して登場
Mixed Reality ToolkitからAdd to Scene Configure を選択(その上のchannel pickerが表示されないのはなぜだ・・・)
なんかわからんけどダメだった・・・
一回Unityを再起動してリトライ・・・
うおおおきたあああ。
結論いうと、Unityを管理者権限で起動したらいけました。UserDIrの外で作業していたもので、何かのファイルが参照権限がなくて見れなかったようだ。情けない。できた物からHolo2sample.slnファイルをVisualstudio 2019で開く
こんな感じでデバイスをHoloLens 2 Emulatorにセットしていざ実行。
待つこと数分・・・
エミュレーターテスト pic.twitter.com/fVoZ7uzgWF
— AKY510 (@AKY5101) 2019年12月11日Hololens2 Emulator が立ち上がってうまいことデプロイ成功しました。
空っぽなので何の面白みもないですが、疲れたので今日はこのあたりで。これで一応準備完了で、今後タッチできるUIを表示させるサンプルや月面着陸するサンプルがあるみたいなので暇を見つけてやっておこうとおもう。
感想
昔HoloToolKitとUnityの相性で苦しんだ思い出があるので、
どうせ手順通りにやってもうまくいかないんだろ~って思ってチュートリアルで詰まったところを記事にしようと思っていたのですが、
思ったよりスルスル行けてしまいました。結果的にこの記事は薄くなってしまった。とはいえ、セットアップの大変さ。
UnityとVSで2度のビルド、エミュレータのとてつもない動作の重さ。
もうちょっと親切にならないかなぁ… 普段のアプリ開発環境とのギャップにだいぶ苦しむ…参考リンク
https://docs.microsoft.com/ja-jp/windows/mixed-reality/mrlearning-base
- 投稿日:2019-12-13T02:31:27+09:00
【Unity, C#】internalな型やメンバにアクセスするには、多分これが一番早いと思います
この記事は【unityプロ技】 Advent Calendar 2019の13日目の記事です。
TL;DR
- C#には
IgnoresAccessChecksToAttribute
っていう隠された属性があるよ。ググってもほとんど情報出てこないよ。- この属性を使えば、外部アセンブリのinternal要素に対して自由にアクセスできるよ。ヤバいよ。 (※12/13 15:00 訂正あり)
InternalsVisibleToAttribute
やAssemblyDefinitionFile
と組み合わせることで、インテリセンスやブレークポイントも効くよ。リフレクションより格段に便利だよ。おさらい:internalアクセス修飾子
C#にはアクセス修飾子と呼ばれる、型やメンバのアクセスを制限する仕組みがあります。
例えばクラスAからクラスBのprivateメソッドを呼ぼうとすると、コンパイルの時点で怒られますし、VisualStudioやVisualStudio Code、Rider等といったIDEのインテリセンスには、候補すら出てきません。error CS0122: 'ApplicationTitleDescriptor' is inaccessible due to its protection level error CS0117: 'EditorApplication' does not contain a definition for 'UpdateMainWindowTitle' ...今回取り上げる
iternal
アクセスレベルは同一アセンブリ(=同じdll)内からのみアクセスできるアクセスレベルです。
このアクセスレベルを持つ型やメンバは、異なるアセンブリ(=別のdll)からは基本的にはアクセスできません。internalな型やメンバにアクセスするモチベーション
internalな型やメンバにアクセスする必要があるの?
公開されている(publicな)APIだけで十分では?
苦労してinternalな型やメンバにアクセスして、何がうれしいの?internalな型やメンバは、ライブラリ製作者がライブラリ利用者に対して隠蔽したい項目であり、APIやドキュメントは一般的に非公開です。
例えば、Unityに何らかのエディタ拡張を加えたい場合は、以下のようなUnityが公開しているAPI(publicなAPI)の利用が基本です。
- メニューの実行:EditorApplication.ExecuteMenuItem
- アセットインポートの事前/事後処理:AssetPostprocessor
- インスペクタや自前ウィンドウの実装:EditorWindow
- 次のフレームでアクションを実行する:EditorApplication.delayCall
- ツールバーにメニューを追加する:MenuItem
- コンテキストメニューを追加する:ContextMenu
- コンパイル後にスクリプトを実行する:Callbacks.DidReloadScripts
- パッケージマネージャUIの拡張:PackageManager.UI.IPackageManagerExtension
- etc.
御存知の通り、公開されているAPIだけでも「出来ないことなんて何も無いんじゃないの?」と思えるほど大量の項目が見つかります。
しかし、公開されていない(=非publicな)APIに目を向けると、さらに多くの事が実現可能なことに気づきます。例えば、【Unity】UIElements で Game ビューにテキストやボタンを追加するエディタ拡張のサンプル(@baba_s様)では、リフレクションを利用して、Unityが公開していないGameViewクラスへアクセスし、GameビューにオリジナルのGUIを追加しています。
var assembly = typeof( EditorWindow ).Assembly; var type = assembly.GetType( "UnityEditor.GameView" ); var gameview = EditorWindow.GetWindow( type ); ...このように、非publicなUnityの世界を知ることは、新しい気づきや便利な拡張機能のヒントになります。
そして、あなたが発見した気づきや便利な拡張機能は、ほかの誰かのヒントになるかもしれません。
ただし、大事なことなので二回言いますが、internalな型やメンバは、ライブラリ製作者がライブラリ利用者に対して隠蔽したい項目であり、APIやドキュメントは一般的に非公開です。
ググって得られる情報は限定的ですし、無慈悲にもAPIは通告なく変更されます。
IL化や難読化されていることは日常茶飯事で、誤って使えばフリーズする可能性すらあります。
ライブラリのライセンスによっては、明確に逆コンパイルやリバースエンジニアリングが禁止されています。ありがたいことに、Unityは公式でGitHubにソースコードを公開しています。internal要素も探し放題です。太っ腹だね!
さあ、実現したいことをキーワードにソースコードを検索し、それっぽい名前の APIを見つけ、注意深くソースコードを読み、拡張機能の幅を
くれぐれも自己責任で広げましょう!開発環境
本記事では以下の環境でデモプロジェクトを開発しました。
- macOS Mojave 10.14.6
- Unity 2019.2.15f1
- VisualStudio For Mac 8.3.8
- DotNet Core 3.0.100
デモプロジェクトは以下のリポジトリで公開しています。
https://github.com/mob-sakai/MainWindowTitleModifierForUnity今回のデモでやること
今回は、Unityエディタのタイトルバーのテキストを変更してみたいと思います。
タイトルバーに現在のブランチ名やゲームサーバの接続先環境名(sand/stag/prod等)を表示できると素敵ですよね。
ソースコードを見たところ、Unity2019.2から追加されたinternalな型とメンバを使えば簡単に実現できそうです。
具体的には、EditorApplication.updateMainWindowTitle
イベントに新しいコールバックを追加し、EditorApplication.UpdateMainWindowTitle()
を呼び出せば、タイトルバーのテキストを変更できそうです:// こんな感じで実装すれば実現できそう Action<ApplicationTitleDescriptor> cb = x => x.title = "なにかしらのエモいタイトル"; EditorApplication.updateMainWindowTitle += cb; EditorApplication.UpdateMainWindowTitle(); EditorApplication.updateMainWindowTitle -= cb;
updateMainWindowTitle
、UpdateMainWindowTitle
、ApplicationTitleDescriptor
は全てinternalな型とメンバであり、このままでは当然コンパイルエラーになります。error CS0122: 'ApplicationTitleDescriptor' is inaccessible due to its protection level error CS0117: 'EditorApplication' does not contain a definition for 'UpdateMainWindowTitle' ...なんとかしてアクセスしてみましょう!
【方法1】Reflection
publicでない型やメンバにアクセスするための方法として、まず思いつくのはReflection(リフレクション)です。
リフレクションにより、文字列を使ってpublicでない型やメンバにも動的にアクセスできます。リフレクションの利点は、internalな型やメンバだけでなく、privateな型やメンバにもアクセスできることです。
また、有名であるが故にググれば情報がたくさんあり、リフレクションだからこそできる事も多くあります。
C#リフレクションTIPS 55連発(@gushwell様)という一読すべき記事もあります。リフレクションを使ってinternalアクセスしてみる
実際にやりたいことは4行で済む内容でしたが、リフレクションで表現するとどうなるでしょうか?
using System; using System.Linq; using UnityEditor; using System.Reflection; namespace MainWindowTitleModifier { public class Solution1_Reflection { [MenuItem("MainWindowTitleModifier/Solution1_Reflection", priority = 1)] static void Update() { // ApplicationTitleDescriptorのTypeを取得. Type tEditorApplication = typeof(EditorApplication); Type tApplicationTitleDescriptor = tEditorApplication.Assembly.GetTypes() .First(x => x.FullName == "UnityEditor.ApplicationTitleDescriptor"); // 関係するイベントとメソッドのInfoを取得. EventInfo eiUpdateMainWindowTitle = tEditorApplication.GetEvent("updateMainWindowTitle", BindingFlags.Static | BindingFlags.NonPublic); MethodInfo miUpdateMainWindowTitle = tEditorApplication.GetMethod("UpdateMainWindowTitle", BindingFlags.Static | BindingFlags.NonPublic); // Action<object>をAction<ApplicationTitleDescriptor>に変換. Type delegateType = typeof(Action<>).MakeGenericType(tApplicationTitleDescriptor); MethodInfo methodInfo = ((Action<object>)UpdateMainWindowTitle).Method; Delegate del = Delegate.CreateDelegate(delegateType, null, methodInfo); // UpdateMainWindowTitleを呼び出す前後にイベントの追加/削除. eiUpdateMainWindowTitle.GetAddMethod(true).Invoke(null, new object[] { del }); miUpdateMainWindowTitle.Invoke(null, new object[0]); eiUpdateMainWindowTitle.GetRemoveMethod(true).Invoke(null, new object[] { del }); } static void UpdateMainWindowTitle(object desc) { // UnityEditor.ApplicationTitleDescriptor.title = "Solution1_Reflection"; と同様 typeof(EditorApplication).Assembly.GetTypes() .First(x => x.FullName == "UnityEditor.ApplicationTitleDescriptor") .GetField("title", BindingFlags.Instance | BindingFlags.Public) .SetValue(desc, "Solution1_Reflection"); } } }うーん、つらみ。。。
やりたいことは単純なはずなのに、どうしてこうなった!って感じですね。ともあれ、
MainWindowTitleModifier > Solution1_Reflection
を実行すればタイトルバーのテキストを変えられるようになりました。
リフレクションの欠点
リフレクションの欠点は生産性が低いことと、実行速度が遅いことです。
やりたいことに対してやるべき準備が多く、IDEによるインテリセンスも効かないため、生産性はお世辞にも良くありません。
なぜリフレクションは遅いのか(POSTD様)によると、静的な呼び出しと比べて2〜3桁オーダーで実行速度が遅いようです。単純なメソッド呼び出しやsetter/getterについて、リフレクションを使う分には悩むことも少ないでしょう。
ジェネリック、コンストラクタ、in/out/ref/paramsキーワード、関数のオーバーロード、非publicな型を含むAction<T>
やFunc<T>
、event等、要素が複雑になると、だんだんと長く、ツラくなってきます。めげずに頑張ってググりましょう。おまけ:リフレクションを効率化して使う
Type
やMemberInfo
のインスタンスをDictionaryでキャッシュする
- 一度生成した
Type
やMemberInfo
のインスタンスを使いまわすことで、オーバーヘッドを回避します- FindMethod、FindProperty等のメソッドをラップする
- Public/NonPublic、Instance/Staticを指定しなくても適切な
MemberInfo
を返すようなメソッドを作り、可読性を改善します- アセンブリからではなく
string
から型を見つける
Type.GetType("UnityEditor.ApplicationTitleDescriptor, UnityEditor")
のように、クラス名フルパス, アセンブリ名
という文字列でも型を取得できます- このときの文字列は
Type
インスタンスを使って、type.AssemblyQualifiedName
で取得できます- デリゲート化、式木、IL Emit
- [雑記] 動的コード生成のパフォーマンス(++C++; // 未確認飛行 C様)を参照してください
【方法2】InternalsVisibleToAttribute
InternalsVisibleToAttribute
は、特定のアセンブリに対して、自身のinternalアクセスを許可させる属性です。
アセンブリに対する単体テストや、機能拡張に使うことが多く、そういったアセンブリはフレンドアセンブリと呼ばれます。
この属性の強力なところは、internalな要素をそのまま記述でき、VisualStudioやVisualStudio Code、Rider等のIDEでインテリセンスやブレークポイントが有効になることです。
テストコードを書くに当たり、IDEのサポートが得られるのはありがたいですね!
さて、
InternalsVisibleToAttribute
は、UnityEngine.dllやUnityEditor.dllといったUnity公式のライブラリにも存在します。
例えばUnityEditor.dllのAssemblyInfo.csには次のように定義されています。[assembly: InternalsVisibleTo("Unity.LiveNotes")] [assembly: InternalsVisibleTo("Unity.Burst")] [assembly: InternalsVisibleTo("Unity.Burst.Editor")] [assembly: InternalsVisibleTo("Unity.Cloud.Collaborate.Editor")] [assembly: InternalsVisibleTo("Unity.CollabProxy.Editor")] [assembly: InternalsVisibleTo("Unity.CollabProxy.EditorTests")] [assembly: InternalsVisibleTo("Unity.CollabProxy.UI")] [assembly: InternalsVisibleTo("Unity.CollabProxy.UI.Tests")] [assembly: InternalsVisibleTo("Unity.CollabProxy.Client")] [assembly: InternalsVisibleTo("Unity.CollabProxy.Client.Tests")] [assembly: InternalsVisibleTo("UnityEditor.Advertisements")] [assembly: InternalsVisibleTo("Unity.PackageManager")] ... //(以下、たくさんのInternalsVisibleToが羅列されている) [assembly: InternalsVisibleTo("Unity.InternalAPIEditorBridgeDev.001")] [assembly: InternalsVisibleTo("Unity.InternalAPIEditorBridgeDev.002")] [assembly: InternalsVisibleTo("Unity.InternalAPIEditorBridgeDev.003")] ...これらの名前を持つアセンブリは、UnityEditor.dllのフレンドアセンブリです。
よって、上記の名前を持つアセンブリを生成できれば、UnityEditor.dllに対してinternalアクセスできます。すっごーい!Unityでアセンブリを生成するための仕組みといえば、AssemblyDefinitionFileです。
AssemblyDefinitionFile
については@toRisouP様がUnity Assembly Definition 完全に理解したにて詳しく解説されております。(こちらも【unityプロ技】 Advent Calendar 2019の記事です)なお、
AssemblyDefinitionFile
によって生成されるアセンブリ名は、インスペクタ内のName
フィールドで設定します。
アセット名ではないことに注意しましょう。
InternalsVisibleTo
を使ってinternalアクセスしてみる適当な
AssemblyDefinitionFile
を作成し、インスペクタビューからName
を変更しましょう。
ここでは、Unity.InternalAPIEditorBridgeDev.001
という、実にそれっぽい名前(おそらくUnityの中の人がテストするためのアセンブリ名)を指定します。
次に、
Unity.InternalAPIEditorBridgeDev.001.asmdef
があるディレクトリ以下に、次のようなスクリプト(Solution2_InternalsVisibleToAttribute.cs
)を追加し、Unity.InternalAPIEditorBridgeDev.001
アセンブリに組み込みましょう。
using System; using UnityEditor; namespace MainWindowTitleModifier { public class Solution2_InternalsVisibleToAttribute { [MenuItem("MainWindowTitleModifier/Solution2_InternalsVisibleToAttribute", priority = 2)] static void Update() { Action<ApplicationTitleDescriptor> cb = x => x.title = "Solution2_InternalsVisibleToAttribute"; EditorApplication.updateMainWindowTitle += cb; EditorApplication.UpdateMainWindowTitle(); EditorApplication.updateMainWindowTitle -= cb; } } }リフレクションが完全に消え、イメージしたようなスッキリしたコードになりました!
直接呼び出しになるので、実行速度も申し分ありませんし、IDEのサポート(インテリセンスやブレークポイント)によって格段に生産性が上がりました。
MainWindowTitleModifier > Solution2_InternalsVisibleToAttribute
を実行すれば、同様にタイトルバーのテキストを変えられます。
InternalsVisibleTo
の欠点
AssemblyDefinitionFile
とInternalsVisibleToAttribute
を使ったinternalアクセスは、非常に手軽な方法です。
しかし、残念ながら、全てにおいて完璧とは言えません:
- privateアクセスができない
- 必要な場合、リフレクションを使ってください
- ライブラリ側に
InternalsVisibleToAttribute
が定義されている必要がある
- 「ライブラリ側に」属性があることが条件です
- 例えばライブラリのアップデート等で、ライブラリ側から
InternalsVisibleToAttribute
が変更・削除されると、コンパイルエラーになります- ランタイムでは使用できない場面が多い
- UnityEngine.dll等は、ビルド時に
InternalsVisibleToAttribute
が綺麗サッパリなくなります- これは、エディタ用とは異なるランタイム用アセンブリが使用されるためです
- アセンブリ名が衝突するとエラー
- パッケージを外部に公開する場合、誰かが同じフレンドアセンブリの名前を使い、アセンブリ名が衝突する可能性があります
- これは潜在的な不具合と言えます
難儀な欠点が多いですが、逆に言えば、クローズドな環境でエディタ向けの機能を実装するのであれば、この方法だけでも十分に使えそうです。
【方法3】IgnoresAccessChecksToAttribute
IgnoresAccessChecksToAttribute
は、ほとんど情報が出回っていない(MSDNにも載ってない)ミステリアスな属性です。
私はNo InternalsVisibleTo, no problem – bypassing C# visibility rules with Roslynで偶然知りました。
要約すると:
InternalsVisibleToAttribute
とは逆の方向に作用する。つまり、ライブラリ利用者側に設定することで、ライブラリに対するinternalアクセスを許可する(※12/13 15:00 訂正あり)- フルネームは
System.Runtime.CompilerServices.IgnoresAccessChecksToAttribute
- Base Class Libraryに載っていないが、ランタイム(CLR/CoreCLR)で作用する
csc.exe
やMsBuild
を使わずに自力でコンパイルする際に、CSharpCompilationOptions.TopLevelBinderFlags
に対して特定のフラグを立てると有効になるつまり、どんなライブラリに対してもinternalアクセスができるようになる、夢のような属性ってことですね!
さっそく、この属性を使ってみましょう。
IgnoresAccessChecksToAttribute
を使ってinternalアクセスしてみる先述の記事にある通り、
IgnoresAccessChecksToAttribute
を使うにはcsc.exe
やMsBuild
を使わずに、自力でコンパイルする必要があります。
自力でコンパイルする方法を紹介しますが、正直めんどくさいです。
興味のない方は「IgnoresAccessChecksToの準備がめんどくさいのでなんとかする」まで読み飛ばし推奨。
まず、C#プロジェクトに以下のコードを追加します。
こうすることで、「UnityEditorアセンブリに対するアクセスレベルのチェックを無視する」ようにコンパイラが解釈できます。[assembly: System.Runtime.CompilerServices.IgnoresAccessChecksTo("UnityEditor")] namespace System.Runtime.CompilerServices { [AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple = true)] internal class IgnoresAccessChecksToAttribute : System.Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { AssemblyName = assemblyName; } public string AssemblyName { get; } } }次に、C#プロジェクトとは別に、「アクセスチェックを無視できる」コンパイラのプロジェクトを作ります。コードは以下の通りです。
// あらかじめMicrosoft.CodeAnalysis.CSharpをnugetでインストールしておく using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using System; using System.IO; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; public class InternalAccessibleCompiler { public static void Main (string [] args) { string inputCsProjPath = args [0]; string outputAsemblyPath = args [1]; string outputAsemblyName = Path.GetFileNameWithoutExtension (outputAsemblyPath); // C#プロジェクトを読み込みます. string[] csproj = File.ReadAllLines(inputCsProjPath); // dllとしてコンパイルさせるオプションを生成します. CSharpCompilationOptions compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) .WithMetadataImportOptions(MetadataImportOptions.All); // BindingFlags.IgnoreAccessibility(1 << 22)を有効化します. typeof(CSharpCompilationOptions) .GetProperty("TopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic) .SetValue(compilationOptions, (uint)1 << 22); // プロジェクトからアセンブリ参照一覧を取得→IEnumerable<PortableExecutableReference>に変換します. Regex reg_dll = new Regex("<HintPath>(.*)</HintPath>", RegexOptions.Compiled); IEnumerable<PortableExecutableReference> metadataReferences = csproj .Select(line => reg_dll.Match(line)) .Where(match => match.Success) .Select(match => match.Groups[1].Value) .Select(path => MetadataReference.CreateFromFile(path)); // プロジェクトからシンボル一覧を取得→IEnumerable<string>に変換します. Regex reg_preprocessorSymbols = new Regex("<DefineConstants>(.*)</DefineConstants>", RegexOptions.Compiled); IEnumerable<string> preprocessorSymbols = csproj .Select(line => reg_preprocessorSymbols.Match(line)) .Where(match => match.Success) .SelectMany(match => match.Groups[1].Value.Split(';')); // プロジェクトからソースコード一覧を取得→テキストとして読み込み→IEnumerable<SyntaxTree>に変換します. CSharpParseOptions parserOption = new CSharpParseOptions(LanguageVersion.Latest, preprocessorSymbols: preprocessorSymbols); Regex reg_cs = new Regex("<Compile Include=\"(.*\\.cs)\"", RegexOptions.Compiled); IEnumerable<SyntaxTree> syntaxTrees = csproj .Select(line => reg_cs.Match(line)) .Where(match => match.Success) .Select(match => match.Groups[1].Value.Replace('\\', Path.DirectorySeparatorChar)) .Select(path => Path.Combine(inputCsProjDir, path)) .Select(path => CSharpSyntaxTree.ParseText(File.ReadAllText(path), parserOption, path)); // コンパイルを実行し、dllを生成します. CSharpCompilation.Create(outputAsemblyName, syntaxTrees, metadataReferences, compilationOptions) .Emit(outputAsemblyPath); } }なお、このコードはコンパイル対象がUnityが生成したC#プロジェクトであることを前提にしています。
他のジェネレータで生成されたC#プロジェクトをコンパイルできないかもしれません。
(きっと、おそらくMSBuildパッケージを使えばたぶん解決できます。)dotnetを使って実行し、以下のようにC#プロジェクトファイルと出力パスを指定すると、internalアクセス可能なdllを生成できます。
dotnet run -- C#プロジェクトパス 出力dllパス
なお、このコンパイラはnuget toolとしても公開しています。(オプションが若干違います)
https://www.nuget.org/packages/InternalAccessibleCompiler/
IgnoresAccessChecksTo
の準備がめんどくさいのでなんとかする
IgnoresAccessChecksTo
をUnityで使うには準備がめんどくさいです。
今回は、先述のコンパイラを同梱済みのUnity向けに公開しているパッケージを使います。
(デモプロジェクトには既にインストールされています。)
- このパッケージには
dotnet 2.1
以上が必要です
- コマンドプロンプト(Windows)やターミナル(Mac)で、
dotnet --version
を実行したときに、2.1.xxx
以上が表示されていればインストール不要です- https://dotnet.microsoft.com/download からインストールしてください
- manifest.jsonの
dependencies
に"com.coffee.internal-accessible-compiler": "https://github.com/mob-sakai/InternalAccessibleCompilerForUnity.git"
を追加し、パッケージをインストールします- プロジェクトビューで
MainWindowTitleModifier.asmdef
を選択し、コンテキストメニュー(右クリック)からInternal Accessible Compiler > Setting
をクリックします![]()
- 開いたウィンドウで、以下のように入力します
Compile
をクリックし、dllにコンパイルします
AssemblyDefinitionFile
やソースコードは、【方法2】InternalsVisibleToとアセンブリ名やクラス名が被らないようにする以外、ほとんど同じです。
コンパイルが完了すると、MainWindowTitleModifier.dll
が生成されます。
このままでは同じ名前を持つMenuItem
が衝突してしまうので、MainWindowTitleModifier.asmdef
のDefine Constraints
に適当な文字列を入力して、インポートされないようにしておきましょう。
MainWindowTitleModifier.dll
のインポート後、MainWindowTitleModifier > Solution3_IgnoresAccessChecksToAttribute
を実行すれば、タイトルバーのテキストを変えられます。
IgnoresAccessChecksTo
の欠点どんなアセンブリに対してもinternalアクセスができるため、この方法は非常に強力です。
【方法2】InternalsVisibleToの欠点も少し克服していますが、それでも完璧ではありません:
privateアクセスができない(※12/13 15:00 訂正あり)
必要な場合、やはりリフレクションを使ってください- 使うためにはコンパイルする必要がある
- コンパイルという手間が発生します
- 自動化するなり、運用でカバーしましょう
- コード内で
UNITY_2019
等のシンボルを使う場合、各バージョンのdll生成が必要
- コード内のシンボルは上記コンパイルの際に解決されます
- そのため、UnityのバージョンアップでAPIが変更されると、APIのバージョン毎にdllを複数生成する必要があります
- 生成したdllのインポート設定の
Define Constraints
から、特定のバージョンのdllのみをインポートするようにしましょう- 自動化するなり、運用でカバーしましょう
これまで紹介したinternal要素へのアクセス方法まとめ
今回は外部アセンブリからinternalアクセスする方法を3つ紹介しました。
方法 利点 欠点 private
アクセス生産性 実行速度 ランタイム 【方法1】
Reflection情報がたくさんある
privateアクセス可
コードの可読性・保守性が低い
低速な実行速度
インテリセンスが無効
【方法2】
InternalsVisibleTo手軽
インテリセンスが有効
高速な実行速度
privateアクセス不可
アセンブリ名が被るとエラー
ライブラリ側に属性が必要
【方法3】
IgnoresAccessChecksToprivateアクセス可
どんなライブラリに対しても有効
インテリセンスが有効
高速な実行速度
自力コンパイルが必要
シンボルの取り扱いに注意が必要
以下は、パッケージマネージャのUIを拡張するパッケージ、UpmGitExtensionを実装した際のワークフローです。
(実際にはdll生成の工程を自動化したり、AssemblyDefinitionFile
の参照関係を整理したり、dllを生成するために使ったディレクトリをパッケージからパージしてます。)
- 一旦、手軽な方法である【方法2】InternalsVisibleToで実装する
- 後でパージしやすいように、パッケージディレクトリに専用のディレクトリを作っておく
- パッケージを公開する前に【方法3】IgnoresAccessChecksToでdllを生成する
- nugetに公開しているInternalAccessibleCompilerとシェルスクリプトで一括処理
- パッケージから「dllを生成するために使ったディレクトリ」をパージし、公開
.npmignore
にディレクトリを記載しておき、npm pack
してパージする- packしたものはnpmレジストリにそのまま公開できる
- packしたものを展開し、upmブランチにプッシュする
- どうしてもprivateアクセスが必要な場面は【方法1】Reflectionを使う
- Exposeクラスのような、リフレクション機能をラップするクラスを作っておくと楽
Expose.FromType(typeof(EditorApplication)).Call("UpdateMainWindowTitle");
のように呼び出せる雑記
UpmGitExtensionの実装において、internal要素へのアクセスは必須事項でした。
UnityやPackageManagerUIのAPIが日々アプデされる状況において、リフレクションのみで対応を続けるのは苦しいものでした。
製作者が見ても意味のわからないコード、名前か引数が変わりエラーを吐き続けるAPI、直ったと思いきや別ベクトルから迫りくるデグレ...
UnityやPackageManagerUIがマイナーアップデートする度にAPIは変更され、どこからか不具合が起き、改修を迫られ、心が折れかけました。2019.3なんて中身ほとんど別モンやんけ!そんな中、今回紹介した方法を使うことで、手軽にinternalにアクセスできるようになり、テストやソースコードの使いまわし(UnityEditor内部のMiniJsonを使ったり)が容易になり、生産性やコードの見通しが格段に改善され、お肌の調子も良くなりました。
もし、あなたの近くでC#のリフレクションに苦しんでいるエンジニアを見かけましたら、そっと本記事のリンクをDMしてあげてください。終わりに
最後に、本当に大事なことなので三回言いますが、internalな型やメンバは、ライブラリ製作者がライブラリ利用者に対して隠蔽したい項目であり、APIやドキュメントは一般的に非公開です。
ライブラリ製作者は、決して意地悪でinternalを使っているのではありません。
不用意にその型やメンバを扱うと、とんでもない結果になるかもしれないからinternalで隠蔽しているのかもしれません。
internalな型やメンバは、「そうあるべき正当な理由があってinternalになっている」ということを理解した上、くれぐれも自己責任で活用してください。皆様、よいUnityライフを!
12/13 15:00訂正
まぁ私もこないだまで知らなくて教えてもらったのですがが。vs-mefとかで投下されてて効果大らすぃ。 https://t.co/w6AzJoxK4y
— neuecc (@neuecc) December 13, 2019Twitterで@neuecc様からリプライをいただいたのですが、リンク先の「*AllowPrivate」というワードが引っかかりました。
参照しているソースコードを読む限り、IgnoresAccessChecksTo
以上のことはしていない様子...
これらの事実から導き出される答えは...
IgnoresAccessChecksTo
はprivateアクセスもできるということです!軽くテストしてみたところ、確かにIDE上ではprivateアクセスがエラーとして表示されましたが、InternalAccessCompilerではエラーなくコンパイルできていました。
もちろん動作も問題ありませんでした。具体的なワークフローの更新については別記事でまとめますので、もう少々お待ちくださいm(_ _)m。
- 投稿日:2019-12-13T00:10:58+09:00
[C#] Span<T>構造体からref Tを取得する方法の違い
普通に使う分には
ref span[0]
が良さそう。
メソッド IsEmpty == true
ReadOnlySpan
の場合ref span[0]
IndexOutOfRangeException
不可( ref readonlyは可
)Span<T>.GetPinnableReference()
Unsafe.AsRef<T>(null)
不可( ref readonlyは可
)IntelliSenseでは出てこない MemoryMarshal.GetReference(Span<T>)
Unsafe.AsRef<T>(null)
可