- 投稿日:2021-01-27T19:51:06+09:00
クラス継承の利点とは何か?(5年間考えた)
3日間考え直した版を投稿しました。
コメントいただいた内容をもとにがんばって考え直してみました。
再考するきっかけとなったコメントは当記事本編より参考になるので是非ご参照ください。
継承のメリットとはなんですか?
あなたはこの質問に答えられますか。
この質問は、未経験歓迎の求人面接でいただいた質問です。当時、苦し紛れに「コード量が減る…?」と回答しました。(苦笑いされました笑)
それから5年、仕事として開発に取り組んできた自分なりの答えを記録します。※ マサカリ歓迎です
結論
共通点を「抽象化」し、is-a関係をコードで具体的に表現できることです。
上記結論だけでは何のことか、何がメリットなのか、ピンとこないかもしれません。
まずは具体的に見てみましょう。クラス継承の例
オブジェクト指向のよくある例にならって「犬」で考えてみます。
さて、実はこの「犬」という概念が「抽象」です。柴犬であれ、ハスキーであれ「鳴く」「走る」といった共通のできることや属性を持ち、
それらの共通点を抽象化した概念が「犬」と言えます。実際にコードで表現してみましょう。
C#class Dog { public virtual void Bark() { Console.WriteLine("ワンワン!"); } public virtual void Run() { /* 走る処理 */ } } class Shibainu : Dog { } class Husky : Dog { public override void Bark() { Console.WriteLine("bow-wow!"); } }Dog dog = new Shibainu(); dog.Bark(); // ワンワン! dog = new Husky(); dog.Bark(); // bow-wow!抽象化した概念を Dog クラスで表現し、柴犬であれハスキーであれ犬として抽象的に扱えていますね!(同じ dog として扱えています)
……しかし、まだメリットが見えてこないかもしれません。
継承のメリットってこうじゃないの?
- 親クラスを修正すると子クラスにも波及して、修正箇所が減らせるよ
- 親クラスに共通関数を定義できるよ
- 結果的にコード量を削減できるよ
はい。たしかに、これらが「継承のメリット」なのは間違いないと思います。
ですが、これらは「抽象化」の結果、生じうるメリットだと考えています。実際、無理やり利点のみを享受しようとして継承すると破綻してしまうでしょう。
たとえば、まったく違う「概念」のクラスを「たまたま似ていた処理を共通化させたいから」と継承してしまったら?先ほどの例でいうと「柴犬」は「走る」という処理が可能でしたが「自動車」も「走ること」自体はできます。だからと言って「プリウス」に「犬」を継承してはいけません。「犬」の特性をすべて持つ車になってしまいます。
当たり前ですが、「Prius is NOT a Dog.」ですよね。
is-a関係が成立するかどうかが、継承で実装するか否かの判断基準の一つです。そのため「継承のメリット」は「抽象化して表現できること」であり、
見えやすいメリットは「抽象化」に付随して生まれてくると結論づけました。今、面接で同じ質問をされたら?
ここまで読み進めていただきありがとうございます。(もうちょっとだけ続きます)
今回の記事の結論では、具体的なソースの確認が必要だったので、言葉足らずでしたね。ですので、同じ質問されたとしたら、以下のような回答をしたいと思います。
継承のメリットとはなんですか?
実装したいビジネスロジックを抽象化してスーパークラスを定義することで関係性を明示でき、ポリモーフィズムを活用できるようになります。
その結果として、保守性や開発効率の向上が見込めることです。・・・少しはマシな回答になったでしょうか?
継承以外で共通関数はどうやって実現するの?
さて、is-a関係にないオブジェクト間の共通機能を継承で実現することを否定しました。
ではどうやって実現するのが最適か?私の意見ですが、
- 複数のクラスで使用したい業務ロジックは、コンポジションによる実現
- I/O操作やDBのトランザクション制御など、業務外のロジックは utility クラスで実現
するのが最適だと考えています。
コード例をどうぞ。コンポジション例(ユーザー検索をA画面でもB画面でも利用したい)interface IUserSearch{ User GetUser(Guid Id); } class UserSearch : IUserSearch { public User GetUser(Guid Id) { return null/*検索結果*/; } } class A { public IUserSearch userSearch { get; } /* ほかに必要なプロパティやメソッドを定義 */ } class B { public IUserSearch userSearch { get; } /* ほかに必要なプロパティやメソッドを定義 */ }このように、共通処理(ユーザー検索)を UserSearch クラスにくくりだし、
プロパティにインスタンスを保持することで利用できるようになり、共通化に成功しました。ほかにも共通化したい処理があれば同じように共通クラスを定義し、プロパティを増やすとよいでしょう。
なお、今回の投稿には関係ありませんが「UserSearch」の初期化は DI がおすすめです。
参考記事:DI (依存性注入) って何のためにするのかわからない人向けに頑張って説明してみる
つづいて業務ロジック以外の場合です。
Utilityクラス例namespace Utilities { public static class Encrypter { public static string Encrypt(string v) { return /* vを暗号化 */; } } }このように、業務ロジックを含まないような共通処理は
独立したクラスをまとめた Utilities 名前空間に含めると収まりがいいと思います。また、実装する前に、同様の機能を提供するAPIがないか探してみると
結果的に時間短縮になるかもしれません。キーワード
これまでの経験や、様々な記事から今回の結論に達しました。
ご興味があれば、以下のようなワードで検索してみてください。
- SOLID原則
- 継承よりコンポジション
- オブジェクト指向エクササイズ
さいごに
「継承はNG」という論も見ますが、利点を見つけられる以上、少し早計だと思います。
ただ、理解せずに使ってしまったり、理屈に振り回されてしまうのもよくありません。一番難しいところですが、適切な手法の選択が大切ですね。
そのためには多くの選択肢を確保しておきたいです。
- 投稿日:2021-01-27T19:25:57+09:00
【C#】ファイルに文字を出力する方法
どうも、Mottyです。
ファイルに文字を出力する方法を簡単にまとめていきます。といっても中身も簡単ですので記事自体は簡潔になりますが、深堀りについては時間があれば追記していきます。書き込む方法
・usingステートメントで、System.IOの使用を宣言します。
・StreamWriterオブジェクトを宣言し、WriteLineで書き込んでいきます。デスクトップにログファイルを書き込む
/// <summary> /// テキストファイルへの書き込み /// </summary> public static void WriteText(string fpath = @"C:\Users\$UserName$\Desktop\logfile.log") //試しにデスクトップにログを書き込んでいきます。 { // テキストファイルのパス using (StreamWriter sw = new StreamWriter(fpath,true))//trueは追記,falseは上書き { sw.WriteLine("NEC"); sw.WriteLine("SONY"); sw.WriteLine("DELL"); sw.WriteLine(text); } }Main.csstatic void Main(string[] args){ WriteText(); Console.ReadKey(); } //output in logfile.log //NEC //SONY //DELLCSVファイルを書き込む
・配列をカンマでConcatしてあげればOK
配列やlistを引数にして関数内で処理しても良いですが、今回は端折りました(汗)public static void WriteText(string content, string fpath = @"C:\Users\$username$\Desktop\csvfile.csv") { using (StreamWriter sw = new StreamWriter(fpath, true))//trueは追記,falseは上書き { sw.WriteLine(content); } }Main.csstatic void Main(string[] args) { var dataList = new List<List<string>>(){ new List<string>() {"1-A","1-B","1-C","1-D"}, // 1行目 new List<string>() {"2-A","2-B","2-C","2-D"}, // 2行目 new List<string>() {"3-A","3-B","3-C","3-D"}, // 3行目 }; string line1 = string.Join(",", dataList[0]); string line2 = string.Join(",", dataList[1]); string line3 = string.Join(",", dataList[2]); WriteText(line1); WriteText(line2); WriteText(line3); Console.ReadKey(); }終わりに
ファイルのテキストを読み込む際はStreamReaderオブジェクトからReadLine()で読み込むこととなりますが、それはまたの機会に。。。
- 投稿日:2021-01-27T17:02:53+09:00
謎の力を棒に与えたかった
抽象的な表現で申し訳ないのですが、棒に謎の力を与える必要がありました。
メモ代わりに使わせていただきます。
- アンカーポイントの変更
- 角度を求めて代入
アンカーポイントの変更
Unityでは基本的にGameObjectの真ん中にセンターが来るようにデザインされています。
今回は物体の端にアンカーポイントを置きたかったので、空のGameObjectを配置して、以下のスクリプトをアタッチ。public class Gizmo : MonoBehaviour { public float gizmoSize = 0.1f; public Color gizmoColor = Color.yellow; void OnDrawGizmos() { Gizmos.color = gizmoColor; Gizmos.DrawWireSphere (transform.position, gizmoSize); } }ヒエラルキーの方は、空のゲームオブジェクトの下に置きたいオブジェクトを配置します。
こちらの記事を参考(というかほぼそのまま)利用させていただきました。
角度を求めて代入
原点はわかっていたので、だいたいこのへんから取ろうと決めた座標を使って角度を求めました。
float rad = Mathf.Atan2(modelpos.y,modelpos.x); float moveY = 90 - rad * Mathf.Rad2Deg;このときに、GameObjectが何でも良いように、
[SerializeField]GameObject targetmodel;上記のように宣言して、参照元が何でも良いようにUnityでフィールドを見れるようにしました。
Quautanionのりお会が全く追いつかなかったため、今回は Quaternion.Euler を使って値を代入しています。
gizmo.transform.rotation = Quaternion.Euler(2.61f,-7.06f,moveY/2);rotationは、Quaternionを代入せねばいけませんが、上記のように雑にVector3みたいなのが代入ができるので便利です。
教えていただいたのはLookAt()関数でしたが、今回は使用しませんでした。以上メモでした。
もっとこうすれば簡単だよ!ということがありましたら教えていただけると幸いです。
- 投稿日:2021-01-27T17:00:11+09:00
Unity C# ランゲーム:敵の攻撃の狙いをランダムにする方法について
こんにちは、連日 Qiitaで質問させて頂いてます。はせがわです。
また、皆さんのお力をお借りしたく質問させて頂きます。
今作っている物
Unityで
「盾一枚で依頼人を敵の攻撃から守りながらゴールまで送り届けるランゲーム」
を作っています。プレイヤーは、自動前進で敵がPlayerまたは依頼人を狙って放物線上に飛んできます。
※現状は、playerのみをターゲットにしています。実現したい事
敵の攻撃は、3パターン作り、そのパターンをランダムで実行したいと思っています
パターン1は、プレイヤーをそのまま狙ってくるパターン
パターン2は、依頼人を狙ってくるパターン
パターン3は、下記のペライチのような、ランダム範囲で攻撃してくる
しかし、現状 Z軸が動いてしまい、盾の後ろの方にターゲットが移動してしまいます。
※現在、ターゲットは、プレイヤーの前身移動速度を考慮して、盾の少し前に設定しています。
Z位置は、そこで固定して YとX軸だけでターゲットがばらついてほしいと思っています。【参考動画】
https://youtu.be/-LhvG5ULkfkクリアしたい課題
・3つ目の ランダム攻撃が実装できません。
どうか、お力を貸してください。。。[Github]
https://github.com/hasegawadesu/ShieldRunner-VR-.git[射出Code]
using System.Collections; using System.Collections.Generic; using UnityEngine; public class throwingscript : MonoBehaviour { //☆ private float InstantiationTimer = 0f; // ターゲットオブジェクトの Transformコンポーネントを格納する変数 public Transform target;//☆ // オブジェクトの移動速度を格納する変数 public float moveSpeed;//☆ // オブジェクトが停止するターゲットオブジェクトとの距離を格納する変数 public float stopDistance;//☆ // オブジェクトがターゲットに向かって移動を開始する距離を格納する変数 public float moveDistance;//☆ //<summary> //testtest //射出するオブジェクト //</summary> [SerializeField, Tooltip("ThrowingObject")] private GameObject ThrowingObject=null; //<summary> //標的のオブジェクト //</summary> [SerializeField, Tooltip("TargetObject")] private GameObject TargetObject=null; //<summary> //射出角度 //</summary> [SerializeField, Range(0F,90F), Tooltip("ThrowingAngle")] private float ThrowingAngle=0; // Start is called before the first frame update private void Start() { Collider collider = GetComponent<Collider>(); if(collider != null) { //干渉しないようにisTriggerをつける collider.isTrigger = true; } } // Update is called once per frame void Update() { // ゲーム実行中に毎フレーム実行する処理 //☆ 変数 targetPos を作成してターゲットオブジェクトの座標を格納 Vector3 targetPos = target.position;//☆ // 自分自身のY座標を変数 target のY座標に格納 //(ターゲットオブジェクトのX、Z座標のみ参照) targetPos.y = transform.position.y;//☆ // オブジェクトを変数 targetPos の座標方向に向かせる transform.LookAt(targetPos);//☆ // 変数 distance を作成してオブジェクトの位置とターゲットオブジェクトの距離を格納 float distance = Vector3.Distance(transform.position, target.position);//☆ // オブジェクトとターゲットオブジェクトの距離判定 // 変数 distance(ターゲットオブジェクトとオブジェクトの距離)が変数 moveDistance の値より小さければ // さらに変数 distance が変数 stopDistance の値よりも大きい場合 if (this.transform.position.z-15 < target.transform.position.z) { Destroy(gameObject); } else if (distance < moveDistance && distance > stopDistance)//☆ { ThrowingBall(); } } //<summary> //ボールを射出する //</summary> private void ThrowingBall() { InstantiationTimer -= Time.deltaTime; if (ThrowingObject !=null && TargetObject !=null && InstantiationTimer <0) { //Ballオブジェクトの生成 GameObject ball = Instantiate(ThrowingObject, this.transform.position, Quaternion.identity); InstantiationTimer =2f; //標的の座標 float x = Random.Range(0.0f, 2.0f); float y = Random.Range(0.0f, 2.0f); float z = Random.Range(0.0f, 2.0f); Vector3 targetPosition = TargetObject.transform.position; TargetObject.transform.position = new Vector3(x, y, z); //射出角度 float angle = ThrowingAngle; //射出速度を算出 Vector3 velocity = CalculateVelocity(this.transform.position, targetPosition, angle); //射出 Rigidbody rid = ball.GetComponent<Rigidbody>(); rid.AddForce(velocity * rid.mass, ForceMode.Impulse); } } //<summary> //標的に命中する射出速度の清算 //</summary> //<param name="pointA">射出開始座標</param> //<param name="pointB">標的の座標</param> //<returns>射出速度</returns> private Vector3 CalculateVelocity(Vector3 pointA, Vector3 pointB, float angle) { //射出角をラジアンに変換 float rad = angle * Mathf.PI / 180; //水平方向の距離x float x = Vector2.Distance(new Vector2(pointA.x,pointA.z),new Vector2(pointB.x, pointB.z)); //垂直方向の距離y float y = pointA.y - pointB.y; //斜方投射のこうしきを初速度について解く float speed = Mathf.Sqrt(-Physics.gravity.y * Mathf.Pow(x,2)/(2*Mathf.Pow(Mathf.Cos(rad),2)*(x * Mathf.Tan(rad)+y))); if(float.IsNaN(speed)) { //条件を満たす初速を算出できなければVector3.zeroを返す return Vector3.zero; } else { return (new Vector3(pointB.x - pointA.x, x * Mathf.Tan(rad), pointB.z-pointA.z).normalized*speed); } } }
なにとぞ、よろしくお願いします。
- 投稿日:2021-01-27T12:17:11+09:00
F# and CSVHelper でCSVファイルを読み込んでみた!
Summary
ここ一年近くCSVファイルを読み込むのに苦労してましたが、
だいぶ形になってきたので書いてみたいと思います
F# and CSVHelper でCSVファイルを読み込んでみました。
動作概要
1. CSVファイル(フルーツ.csv)を読み込む
2. 読込んだレコードを出力する読込元CSV
りんご,赤,300,2020/12/11 チェリー,黒,450,2020/12/12ドラゴンフルーツ,赤紫,"1,200",2020/12/24 キャベツ,緑,,2020/12/30出力例
[|seq [{ ファイル作成日時 = 2021/01/27 11:33:32 果物名 = "ドラゴンフルーツ" 色 = "赤紫" 価格 = "1,200" 購入日 = 2020/12/24 0:00:00 }; { ファイル作成日時 = 2021/01/27 11:33:32 果物名 = "キャベツ" 色 = "緑" 価格 = "" 購入日 = 2020/12/30 0:00:00 }; { ファイル作成日時 = 2021/01/27 11:33:32 果物名 = "りんご" 色 = "赤" 価格 = "300" 購入日 = 2020/12/11 0:00:00 }; { ファイル作成日時 = 2021/01/27 11:33:32 果物名 = "チェリー" 色 = "黒" 価格 = "450" 購入日 = 2020/12/12 0:00:00 }]|]ソースコードの構成
こんな感じ
csvRead_log.fs
エラーになった場合の受け皿を作ります
namespace CsvRead module Log = type ErrorRecordLog = { 処置済 : bool ファイル作成日時 : System.DateTime ファイル名 : string エラーレコード : string }csvRead_util.fs
csvを読み込む際のちょっとした自作便利関数をここに書きます
namespace CsvRead module public CsvUtil = module public Encoding = open System.Text let public shiftJIS = Encoding.RegisterProvider(CodePagesEncodingProvider.Instance) Encoding.GetEncoding 932 let public utf8 = Encoding.GetEncoding 65001 module public Files = open System.IO open System.Text.RegularExpressions let targetFiles1 srcDir fileExtension filePattern : option<string[]> = System.IO.Directory.EnumerateFiles(srcDir, "*" + fileExtension) |> Seq.filter(fun fp -> Regex.IsMatch(fp,filePattern) ) |> Seq.sortByDescending(fun fp -> File.GetLastWriteTime(fp)) |> Seq.map(fun fp -> Path.Combine(Directory.GetCurrentDirectory() , fp ) |> fun s -> Path.GetFullPath(s)) |> fun x -> x |> Seq.toArray |> fun x -> if Array.isEmpty x then None else Some xcsvRead_global.fs
CSVを読み込む際の一般的な情報をここに書きます(フォルダ場所とか)
namespace CsvRead module public GlobalDataStore = open System.Collections.Concurrent // ★ multiple record type の場合は必要に応じて数量を増やす let num = 1 let cqGoods = Array.init num (fun _ -> new ConcurrentQueue<obj>() ) let cqBad = new ConcurrentQueue<obj>() /// ★読込対象のCSVファイルに関する一般設定 module public GlobalSetting = let csvSetting = {| csvFolder = "./../csv" extension = "*.csv" fileRegPattern = ".*フルーツ.*" fileEncoding = Encoding.utf8 |}csvRead_csv.fs
読込対象CSVの構造をここに書きます
namespace CsvRead open System open CsvHelper.Configuration [<CLIMutable>] type public Csv = { mutable ファイル作成日時 : DateTime 果物名 : string 色 : string 価格 : string 購入日 : Nullable<DateTime> } [<Sealed>] type public CsvMap () as this = inherit ClassMap<Csv>() do this.Map(fun x -> x.ファイル作成日時).Constant( System.DateTime(9999,12,31,12,59,59) ) |> ignore this.Map(fun x -> x.果物名).Index(0) |> ignore this.Map(fun x -> x.色).Index(1) |> ignore this.Map(fun x -> x.価格).Index(2) |> ignore this.Map(fun x -> x.購入日).Index(3) |> ignorecsvRead_csvhelperWrap.fs
CSVHelperのラッパーをここに書きます
namespace CsvRead module public CsvHelperWrap = open System.Globalization open System.IO open CsvHelper open CsvHelper.Configuration open GlobalDataStore open CsvRead.Log // ★Csv読込設定 let private csvConfig (fp:string) : CsvConfiguration = CsvConfiguration(CultureInfo.CurrentCulture) |> fun x -> x.Delimiter <- "," x.HasHeaderRecord <- false x.TrimOptions <- TrimOptions.Trim x.IgnoreBlankLines <- true x.ShouldSkipRecord <- (fun arr -> arr.[0] = "EOF" ) // ★Multiple Record type の場合はここをレコードタイプ毎に追加していく x.RegisterClassMap<CsvMap>() |> ignore // カラムが不足していてエラーを出したくない場合は null を設定する x.MissingFieldFound <- null x.BadDataFound <- fun ctx -> [| $"データ番号:{ctx.RawRow} : データ形式がおかしいです BadData" ctx.RawRecord.Trim() |] |> fun errRcd -> cqBad.Enqueue( { 処置済 = false ファイル作成日時 = File.GetCreationTime(fp) ファイル名 = fp |> System.IO.Path.GetFileNameWithoutExtension エラーレコード = errRcd |> String.concat "," } ) x let public csvRead (streamReader:StreamReader) (fp:string) = use csv = new CsvReader(streamReader , csvConfig fp) // ★ヘッダーがある場合 // let skipRows (csv:CsvReader) i = // for j in [1..i] do // csv.Read() |> ignore // skipRows csv 1 while (csv.Read()) do try cqGoods |> Array.item 0 |> fun x -> let tmp = csv.GetRecord<Csv>() tmp.ファイル作成日時 <- File.GetCreationTime(fp) x.Enqueue( tmp ) with _ -> [| $"データ番号:{csv.Context.RawRow} : 何かしらのエラーです" csv.Context.RawRecord.Trim() |] |> fun errRcd -> cqBad.Enqueue( { 処置済 = false ファイル作成日時 = File.GetCreationTime(fp) ファイル名 = fp |> System.IO.Path.GetFileNameWithoutExtension エラーレコード = errRcd |> String.concat "," } )csvRead_main.fs
ファイル処理をここに書きます
namespace CsvRead open GlobalSetting module Main = let public CsvRead1 () = CsvUtil.Files.targetFiles1 csvSetting.csvFolder csvSetting.extension csvSetting.fileRegPattern |> fun x -> match Option.isNone x with | true -> () | false -> Option.get x |> Array.iter(fun fp -> new System.IO.StreamReader( fp , csvSetting.fileEncoding) |> fun sr -> CsvHelperWrap.csvRead sr fp )main.fs
open CsvRead open GlobalDataStore [<EntryPoint>] let main argv = Main.CsvRead1 () cqGoods |> printfn "%A" 0現場からは以上です
- 投稿日:2021-01-27T11:51:01+09:00
【Unity】InputFieldにTabを押したら次へ行くという処理を追加する
SwitchInputField.csusing UnityEngine; using TMPro; public class SwitchInputField : MonoBehaviour { public TMP_InputField thisInputField; public TMP_InputField nextInputField; // Start is called before the first frame update void Start() { thisInputField = GetComponent<TMP_InputField>(); } // Update is called once per frame void Update() { if(thisInputField.isFocused && Input.GetKeyDown(KeyCode.Tab)) { nextInputField.Select(); } } }//使い方
InputFieldコンポーネントを持っているオブジェクト自体にAdd Componentします。
Tabを押した時に選択されて欲しいオブジェクトをnextInputFieldにアタッチ or スクリプトから代入します。//TextMeshProを使わない方向け
using TMPro; => using UnityEngine.UI;
TMP_InputField => InputField
という風に読み替えてください。




