20220225のRubyに関する記事は12件です。

引数をそのまま戻すメソッドを作る際の注意

クイズ:引数をそのまま戻すメソッドを作りなさい。 与えた引数がそのまま戻ってくるメソッドってありますよね、pとか。そんなメソッドを作ってください。ただし引数は 1 つとは限りません。(キーワード引数やブロックは考えないことにします。) つまり、以下のような結果が得られるメソッドです。 return_args() # => nil return_args(1) # => 1 return_args(1, 2) # => [1, 2] クイズ形式にしてみましたが、私自身がそんなメソッドを作った際に引っかかってしまったので戒めの記事です。無駄に長くなってしまったので、結論だけ知りたい人は#結論へどうぞ。 記事中の実行結果はすべて以下のバージョンの Ruby を用いています。 ruby 3.1.1p18 (2022-02-18 revision 53f5fc4236) [x86_64-linux] 引数転送 ... が使えるのでは? Ruby 2.7 で...による引数の転送ができるようになりました。 def foo(...) bar(...) end これを使えばいいのでは? def return_args(...) ... end return_args.rb:2: warning: ... at EOL, should be parenthesized? return_args.rb:3: syntax error, unexpected `end' ダメでした。単なる...は範囲演算子の...と判定されてしまうのでした。 引数転送の...にはカッコが必要らしいので、付けてみましょう。 def return_args(...) return(...) end return_args.rb:2: syntax error, unexpected ')' return(...) やっぱりダメでした。ちなみにreturnを入れてみましたが、入れても入れなくても結果は変わりません。...はあくまでも引数を別メソッドに転送するための記法であり、戻り値には使えないようです。1 Splat 演算子 * で受け取る ...はこうした場合には使えないことが分かったので、Splat 演算子で引数を受け取ることにしましょう。 def return_args(*args) args end 引数が 2 個以上であれば上のコードでいいのですが、引数が 0, 1 個の場合戻り値が配列になってしまってよろしくありません。 return_args() # => [] ←× return_args(1) # => [1] ←× return_args(1, 2) # => [1, 2] ←○ 戻り値にも Splat 演算子を使えばいいのでは? Splat 演算子を使えば、配列を展開できます。 a = [1, 2, 3] foo *a # foo 1, 2, 3 と書いたのと同じ a = [1] foo *a # foo 1 と書いたのと同じ ということは、こうなるのでは? a = [1, 2, 3] return *a # return 1, 2, 3 と書いたのと同じ a = [1] return *a # return 1 と書いたのと同じ…? というわけで、↓ で行ける? def return_args(*args) *args end return_args.rb:2: syntax error, unexpected '\n', expecting '=' *args む、よく分からないが構文的にダメらしい。return付けてみたらどうだろう? def return_args(*args) return *args end エラーが起きない!よし、これで行けた! と思いきや return_args() # => [] ←× return_args(1) # => [1] ←× return_args(1, 2) # => [1, 2] ←○ あれ、* 無しの場合と変わらない… 引数の数で処理を分ける 仕方ない、引数が 1 個以下の場合は別対応しましょう。 def return_args(*args) args.size <= 1 ? args.first : *args end return_args.rb:2: syntax error, unexpected * ...args.size <= 1 ? args.first : *args あれ、また SyntaxError だ。三項演算子だとダメなのか。ifに替えよう。 def return_args(*args) if args.size <= 1 args.first else *args end end return_args.rb:5: syntax error, unexpected '\n', expecting '=' *args そうだった、さっきも同じミスしていたのに… *argsだけではエラーになるのでreturnを付けよう。 def return_args(*args) if args.size <= 1 return args.first else return *args end end できたー!…でも、ずいぶんとゴチャゴチャしてしまったなぁ… 先人の知恵を借りる そういえば、pとかppはどうやっているんだろう? def pp(*objs) # 略 objs.size <= 1 ? objs.first : objs end あれ、これだけでいいの? 制御構造 (Ruby 3.1 リファレンスマニュアル) #return 式が2つ以上与えられた時には、それらを要素とする配列をメソッドの戻り値とします。 なるほど、戻り値が 2 個以上あるときは結局配列になっているのか。だから引数が 2 個以上の場合にはわざわざ Splat で展開しなくても、配列そのままで戻せばいいわけね。 そうか、Splat 演算子を使うという考えにとらわれていたから三項演算子がダメだったりreturn必須だったりしたけれど、使わなければこれだけシンプルに書けるのか。 結論 こうしましょう。 def return_args(*args) args.size <= 1 ? args.first : args end return *argsで行けると思い込んでいたのでテストもしておらず、某ライブラリを 1 年近く適切に動かないまま公開してしまっていました。すみません。 そもそも現実的には「引数をただそのまま戻すメソッド」ではなく「引数を何か処理して、そしてそのまま戻すメソッド」を作るでしょうから、実際の引数にアクセスできない...はそういう意味でも使えません。(「何か処理して」の部分を他のメソッドにぶん投げれば使えますが。) ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

form_withについて

はじめに form_withは初学者にとって理解しずらい部分だと思います。 modelオプションとurlオプションのところで、よく分かってなかったことがありました。 フォームとは ユーザーが情報を入力する入力欄のこと。 アカウント登録ページに出てくる名前とかemailとかを入力する四角い箱の事です。 form_withとは フォーム作りを助けてくれるヘルパーメソッドです。 ヘルパーメソッドとはRailsにおける、主にビューでHTMLタグを出現させたりテキストを加工するために使うメソッドの総称です。 例えば、HTMLで以下のように書くのを <form action="/tweets" method="post"> <input type="text" name="content"> <input type="submit" value"送信"> </form> 以下のように書きます。 <%= form_with url: "/tweets", method: :post, local: true do |form| %> <%= form.text_field :content %> <%= form.submit '送信' %> <% end %> ヘルパーメソッドはRubyとして取り扱われるので、<%= %>で囲んで表示します。 doからendまでの間がフォームの記述になります。 form_withのオプションについて form_with(model: 〇〇, url: 〇〇, method: 〇〇・・・) form_withの後に続く引数(オプション)がたくさんあります。 それぞれフォームがどのような動きをするか決めています。 modelオプションとurlオプションについて ここの理解が不十分でした。 urlオプションはフォームでいうaction属性と同じ役割で、modelオプションはインスタンス(データを保存する変数)をフォームに渡す役割だと思っていました。 間違いではないです。しかし、これだけの認識だとハマります。 なぜならurlオプションの記述がないform_withがあるからです。 データの送り先はどうやって決まるのか。 なぜurlオプションがないのか。 <%= form_with model: @tweet, local: true do |form| %> 実は上記のコードは <%= form_with model: @tweet, url: tweet_path, local: true do |form| %> と同じ意味です。 このように記述できるのには理由があります。 form_withの賢さ form_withは@tweetという変数の中身があるか、ないかで次のアクションを予測しています。 @tweetが空の場合はtweets_path(createアクション)へデータを送る。 @tweetが何かデータを持っていた場合tweet_path(updateアクション)へデータを送る。 このようなことを勝手に行ってくれます。 つまりurlオプションで送り先を指定しなくてもいいのです。 オプション名 意味 modelオプション 入力値を格納するインスタンス(form_withによるaction属性の指定) urlオプション フォームで入力した情報の送信先を指定 methodオプション 情報の転送方法を指定。主にgetかpost localオプション リモート送信を無効にするかどうかを指定。trueにすると無効になる おわりに form_withはまだ分からないことがあります。 また間違いなどございましたら、コメントいただけると幸いです。 お手数お掛けします。よろしくお願い致します。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSのS3で画像が保存できない

簡易的なフリマアプリを作成中。 AWSのS3に画像を保存するための実装を実施。 必要なファイルや記述なども終え、まずローカル環境で画像がS3に保存できるかの挙動確認を行う。 が、エラー発生。 エラー文は以下の通り。 NoMethodError (undefined method `upload' for nil:NilClass): 「スペルミスでもしたかな?」 まずは今回新たに追記したconfig/environments/developmentとconfig/storage.ymlのファイルを見直す。 しかし特に間違っている様子はない。 次にエラー内容をgoogleにて検索。 すると、同じ境遇の方々がたくさんいらっしゃいました笑 それぞれの記事を見ていると「:が抜けている」や「""を忘れている」などの内容が多かったので、再度その辺りを確認するため、各コードを見直し。 が、やはり間違いが見当たらない・・・ S3に入る直前に挙動確認を行った際は、問題なく画像が保存されていたので、コードの書き間違いは考えにくい。 間違いがあるのはconfigの箇所であることはほぼ確定している。 でもどこが間違っているのかが全くわからない・・・ するととある記事を発見。そこには「余計なスペースが入っていた」と書かれている。 「スペース?そういえばそこは意識してなかったかも」 再度コードとにらめっこ開始。 すると・・・ 発見!!! config/storage.yml local: service: Disk root: <%= Rails.root.join("storage") %> amazon: service: S3 region: ap-northeast-1 bucket: バケット名 access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %> secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %> amazon:の前に半角スペースが! 調べてみると、半角スペースがあるせいで、上記のlocal:の続きと見なされてしまうのだとか。 早速この余計なスペースを削除して、再度挙動確認! が、まだ保存できない・・・ さらに1時間考えた末、出した結論は PC再起動 すると無事にエラー解決できました笑
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

テスト

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

【RSpec】テストコードはループを使わない方がベター

なぜループ処理を使わない方がいいのか テストコードは仕様がひと目でわかることが大事なため、可読性が下がるループはしない方がベター describe '#format_date_of_birth' do it '生年月日を和暦で表示すること' do %w(1977-06-06 1988-06-06 1989-06-06 2016-06-06).each do |date_text| date = Date.parse(date_text) person = Person.new('Taro', date) jp_year = date.year >= 1989 ? "平成#{date.year - 1988}年" : "昭和#{date.year - 1925}年" expected = "#{jp_year}#{date.month}月#{date.day}日" expect(person.format_date_of_birth).to eq expected end end end ただ、テストデータを作成するときだけはループ処理で書いてしまった方が見やすい気がする。 「テストはループ処理を使わない方がいい」という言葉だけだとデータ作成までベタ書きするのかなと勘違いしそうになったので。 (Date.new(2021, 1, 1)..Date.new(2021, 1, 15)).each do |date| create(:post, date: date, user: user) end さいごに 記事の中で伊藤さんがおっしゃっていた言葉が印象的だった。 何事も機械的に適用しない。目的を考えて最適な選択肢を選ぶ コードの書き方に絶対はなく、一個人がより良いと思う方を自分で考えて自分なりの信念を持って選択していくことが大事だと思う。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSのEC2の環境構築で参考にしたサイト(rails,docker,nginx)

以下のインフラ環境を構築するまでに参考にしたサイトをご紹介します。 1,AWSの全体的な知識を付けるために参考にしたもの <動画教材> WEBエンジニアの山浦さんのUdemyの動画教材です。 解説が初心者に優しく、わかりやすいのでおすすめです。 <書籍> 解説がわかりやすいです。 イラスト使って説明してくれるので、なにをやっているのかイメージしやすく、すぐに読み終えることができました。 補足: ご紹介した2点は内容が共通している部分が多いです。 山浦さんの動画教材の方が、内容が濃いので、そちら1点だけでも良いかもしれません。 2,VPC作成からEC2にデプロイするまで 有料の教材です。 買っても良いと思いますが、chapter3まで無料なのでそこまでやって、その後は次にご紹介する記事を参考にすると良いと思います。 この記事にはかなり助けられました。 これを進めれば、RDSの作成やdocker環境でwebサーバーがnginxのrailsアプリをEC2にデプロイまでできます。 3,ALBを使用してHTTPからHTTPSでサイトにアクセスできるようにする サイトのHTTPS化には次のサイトを参考にしました。 chapter6でHTTPS化、 chapter7でHTTPでアクセスしてもHTTPSになるように設定できるようになります。 ちなみにサイトのドメインの取得方法は、最初にご紹介した山浦さんの動画で、「お名前.com」でドメインを購入し、Route53と連携する所まで紹介されています。 4,SESを使ったメール送信 AWSのSESを使用してメール送信は以下のサイトを参考にしました。 SES導入はエラーがでまくり、かなり苦戦したので、一応自分のエラーが出なかったproduction.rbを貼っておきます。 config/enviroments/production.rb creds = Aws::Credentials.new( ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACESS_KEY'] ) Aws::Rails.add_action_mailer_delivery_method( :aws_sdk, credentials: creds, region: ENV['AWS_REGION'] ) config.action_mailer.default_url_options = { host: 'あなたが取得したドメイン名' } config.action_mailer.delivery_method = :aws_sdk config.action_mailer.perform_deliveries = true config.action_mailer.perform_caching = false config.action_mailer.raise_delivery_errors = true ポイントは、 config.action_mailer.delivery_method = :aws_sdk の部分です。 rails aws sesで検索すると、ここが「:ses」になっている記事が多いですが、「:aws_sdk」にしないとエラーが出たので、よかったら参考にしてください。 4,S3に画像をアップロードする S3はRailsチュートリアルを参考に実装しました。 最後に 山浦さんのUdemyの講座が全体を網羅しているので、山浦さんの教材を軸にご紹介した記事などを参考に進めれば、環境構築はできると思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

私が作ったオリジナルアプリを公開します!

はじめに スクール卒業後に、独学で完成させました。 まだまだ未熟ですが、誰かの参考や希望になればと思い、投稿します。 ①「冷蔵庫の中身がわかるアプリ」 https://reizoko-app.herokuapp.com/ (ユーザー名: reizoko /パスワード: 11770) [GitHubコード:https://github.com/mina153/reizoko-app ] 冷蔵庫の中身を記録するアプリです。 同年代の方と「どんなアプリが欲しいか」というテーマで討論したときに、出た意見を参考にして、このアプリを作りました。 工夫した点は、  ・ユーザーが飽きないように、CSSを使ったアニメーションを追加した  ・hoverを使うなどして、アプリに動きをつけた  ・テストコードも書き、より実務に近づけた  ・Basic認証でセキュリティを考慮した (今回は誰でも触れるように公開してます。) です。 ②「自分の好きなものについて投稿できるインスタ風のアプリ」 オリジナルアプリ:https://ramen-app-3.herokuapp.com/ (ユーザー名: hoge /パスワード: 3333) [GitHubコード:https://github.com/mina153/ramen-app ] Ruby on Rails を軸に、インスタ風のアプリケーションを1から作りました。 工夫した点は、  ・カレンダー機能  ・いいね機能  ・その他投稿機能  ・Basic認証でセキュリティ配慮(今回は誰でも触れるように公開してます。) です。 最後に このアプリは、TECHCAMPプログラミングスクール卒業後に、独学で完成させました。 「ここの実装気になる」「どうやったらこれができるの!」と思った方がいましたら、ぜひ質問ください !?‍♀️
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「Herokuで画像表示できない」を解決

はじめに  現在オリジナルアプリの開発中です。Heroku環境でアプリをアップロードしているのですが、assetsに配置した画像が表示されないという問題がありました。解決方法がわかったのでまとめておきます。 問題  app/assets/images 配下に設置した画像が表示されない。(ローカル環境では表示されている) ローカル環境では表示されているため、本番環境では追加の設定がいるのかもしれない。と思い調べると解決方法が出てきました。 解決方法  解決方法は image_tagのファイルの指定方法を修正することでした。 qiita.rb <%= image_tag "/assets/top.png" ,class:"img-fluid" %> image_tagに渡す画像のバスの指定方法を間違えていました。 この書き方では、ローカルだと表示されるのですが、本番環境だと表示されません。 以下に修正しました。 qiita.rb <%= image_tag "top.png" ,class:"img-fluid" %> 結果無事に本番環境でも画像を表示する事ができました。 追記 以下の方法は間違いでした 最初自分が採用した以下の解決方法は間違いでした。(画像は表示されるけど推奨されない) 編集リクエストありがとうございます。 production.rb config.assets.compile = false という記述があるので、 production.rb config.assets.compile = true に変更するやり方です。 この方法でも画像を表示できるのですが、本番環境でのパフォーマンスに悪影響を及ぼすため推奨しないとのことです。 以下を参考にしました。 https://qiita.com/jnchito/items/3d225112a3ac95379b1d まとめ  本番環境でのエラーだったため、最初何が原因か特定できなかったのですが、調べたらすぐに出てきました。 分からなくても焦らず、まずは冷静になる。→状況を整理する。→ググる という習慣を身につけたいと思います。 また、解決方法にも様々あるので、精査できるようになりたいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】find_or_initialize_byとfind_or_create_byの違い

find_or_create_by 条件を指定して初めの1件を取得(find)し、1件もなければ作成(create) 作成する時に呼ぶメソッドがcreateなので、新規作成して保存まで行う Category.find_or_create_by!(name: "ディナー") Category Load (0.6ms) SELECT "categories".* FROM "categories" WHERE "categories"."name" = $1 LIMIT $2 [["name", "ディナー"], ["LIMIT", 1]] TRANSACTION (0.5ms) BEGIN Category Create (2.0ms) INSERT INTO "categories" ("name", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["name", "ディナー"], ["created_at", "2022-02-21 12:19:12.868048"], ["updated_at", "2022-02-21 12:19:12.868048"]] TRANSACTION (1.4ms) COMMIT => #<Category:0x00007fed472719b0 id: 1, name: "ディナー", created_at: Mon, 21 Feb 2022 12:19:12.868048000 JST +09:00, updated_at: Mon, 21 Feb 2022 12:19:12.868048000 JST +09:00> Category.find_or_create_by!(name: "その他") Category Load (0.6ms) SELECT "categories".* FROM "categories" WHERE "categories"."name" = $1 LIMIT $2 [["name", "その他"], ["LIMIT", 1]] => #<Category:0x00007fed4483f0e8 id: 2, name: "その他", created_at: Thu, 03 Feb 2022 12:20:21.014342000 JST +09:00, updated_at: Thu, 03 Feb 2022 12:20:21.014342000 JST +09:00> find_or_initialize_by 条件を指定して初めの1件を取得(find)し、1件もなければ作成(new) 作成する時に呼ぶメソッドがnewなので、保存はしない 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[py2rb] 多重継承の6

はじめに 移植やってます。 ( from python 3.7 to ruby 2.7 ) 多重継承 (Python) 前回の記事のコメントにて、@Nabetani さんよりRuby3.1ではクラス変数のオーバーライドがエラーになることを教えていただきました。 親クラスに、子クラスで既に定義されている同名のクラス変数を追加した場合、子クラスが、そのクラス変数を参照した際に例外 RuntimeError が発生します。 また、クラス変数について興味深い記事を投稿されています。 失敗(Ruby) module Base def __init__ @name = 'Base' @base = 'Base' puts 'Base' end end module C4 include Base def __init__ @name = 'C4' puts 'C4' super end end module C2 include C4 def __init__ @name = 'C2' puts 'C2' super end end module C3 include Base def __init__ @name = 'C3' puts 'C3' super end end class C1 include C2, C3 attr_reader :name, :base def initialize __init__ end def __init__ @name = 'C1' puts 'C1' super end end c = C1.new p [c.name, c.base] p C1.ancestors # output C1 C2 C4 C3 Base ["Base", "Base"] [C1, C2, C4, C3, Base, Object, Kernel, BasicObject] 継承チェーンでBaseがC3の後から呼ばれています、よしよし。 クラス変数からインスタンス変数に替えましたが、@nameが継承元で'Base'に書き換えられています、ぐぬぬ。 成功(Ruby) module Base def __init__ @name = 'Base' @base = 'Base' puts 'Base' end end module C4 include Base def __init__ puts 'C4' super @name = 'C4' end end module C2 include C4 def __init__ puts 'C2' super @name = 'C2' end end module C3 include Base def __init__ puts 'C3' super @name = 'C3' end end class C1 include C2, C3 attr_reader :name, :base def initialize __init__ end def __init__ puts 'C1' super @name = 'C1' end end c = C1.new p [c.name, c.base] p C1.ancestors # output C1 C2 C4 C3 Base ["C1", "Base"] [C1, C2, C4, C3, Base, Object, Kernel, BasicObject] superの後でインスタンス変数を初期化することにより、正しいオーバーライドになるようです。 継承の実験 module C4 include Base def __init__ puts 'C4' - super @name = 'C4' end end # output C1 C2 C4 ["C1", nil] [C1, C2, C4, C3, Base, Object, Kernel, BasicObject] 仮に、成功(Ruby)のコードで、module C4のsuperを削除した場合、継承チェーンのC4の次のC3のdef __init__が呼ばれないためputs 'C3'が実行されないことが分かります。 また、その結果Baseのdef __init__も実行されないため、@baseが初期化されていないこともわかります。 追記 成功(Ruby nilガード) module Base def __init__ puts 'Base' @name ||= 'Base' @base ||= 'Base' end end module C4 include Base def __init__ puts 'C4' @base ||= 'newBase' super end end module C2 include C4 def __init__ puts 'C2' super end end module C3 include Base def __init__ puts 'C3' super end end class C1 include C2, C3 attr_reader :name, :base def initialize(name = nil) @name = name || 'C1' __init__ end def __init__ puts 'C1' super end end c = C1.new("newC1") p [c.name, c.base] p C1.ancestors # output C1 C2 C4 C3 Base ["newC1", "newBase"] [C1, C2, C4, C3, Base, Object, Kernel, BasicObject] お馴染みnilガード ||=をmodule内で使用してみました。 これだとsuperのタイミングを気にしなくてよさそう。 メモ Python の 多重継承の6 を学習した 百里を行く者は九十里を半ばとす
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

includesを使ってSQLの発行件数を減らそう(N+1問題)

SQLの発行件数が多いと指摘を受ける。 業務にて、ある一覧画面を作成していたところ、 自分が書いたソースをみた先輩から、 「この一覧画面のSQLの発行件数が多すぎる。ちょっと減らしてみて。」とご指摘を受けました。 SQLの発行件数が多いのはN+1問題が起きていたことが原因で、 includesを使用することで 事象を解決できたのでアウトプットしてみようと思います。 N+1問題とは 必要以上にSQLが実行されて、パフォーマンスが低下する問題。 今回指摘を受けた画面では、 ログを見ると、他のテーブルの情報を取得するため何度もSQLが発行されていました。 includesとは includesメソッドは、関連している複数のテーブルからデータをあらかじめ取得するメソッドです。 N+1問題を解決させるためには、 都度SQLを発行させるのではなく、あらかじめデータをすべて取得しておくことで解決することができます。 例えば、 UserクラスとPostクラスがあって、 投稿からユーザ名を取得する処理があったとします。 # models/user.rb class User < ApplicationRecord has_many :posts end # models/post.rb class Post < ApplicationRecord belongs_to :user end # 投稿の一覧を取得する posts = Post.all posts.each do |post| p post.user.name end 上記のメソッドを実行すると、 投稿の数だけSQL文が発行されてしまい、 効率が悪いです。 そこで、下記のようにincludesメソッドを使用して、 あらかじめ投稿に関するユーザ情報を最初にすべて取得します。 そうすることで、 投稿がいくらあってもSQLの発行を一度で済ますことが可能です。 # 投稿と、投稿に関連したユーザ情報を取得する。 posts = Post.includes(:user) posts.each do |post| p post.user.name end 余談 ※SQLの発行件数は画面に対してAlt+Pを押下したり、  ログを見ることで確認できます。 参考記事: https://techacademy.jp/magazine/22031 https://qiita.com/south37/items/b2c81932756d2cd84d7d
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

クラス変数の持ち主

これは何? クラス変数の持ち主が、 python と ruby で違うことに気がついたので、他の言語どうなってるんだろと思って調査したのでその記録。ジェネリクスとテンプレートを交えながら。 ruby の場合 ruby class B def set_v(x); @@v=x; end def v(); @@v; end end class C < B;end class D < B;end c = C.new.set_v("C#set_v") d = D.new.set_v("D#set_v") p [ C.new.v, D.new.v ] #=> ["D#set_v", "D#set_v"] クラス変数 @@v の持ち主は、 class B 。 C 経由で変更しても、 D から見た @@v の値が変わる。 Python3 の場合 python3 class B: v = "in-class-def" @classmethod def setV(cls,x): cls.v = x class C(B):... class D(B):... print(B().v, C().v, D().v) #=> in-class-def in-class-def in-class-def C().setV("C.setV") D().setV("D.setV") print(B().v, C().v, D().v) #=> in-class-def C.setV D.setV Python のクラス変数はcls.v のようにするので、v の持ち主は cls で指定される型。なので、クラス変数を参照しているメソッドが書かれているクラスとは関係がない。 最初の print 内にある C.v なんかは、C の基底クラスを見に行っている。 わりと珍しい作戦だと思う。 この作戦だと、深い継承ツリーで偶然同じ名前を使ってしまったために大惨事、みたいなことが起きると思う。 ※ 初出時間違ったことを書いてましたすいません。 Java の場合 Generics なしで まずは Generics なしで。 java import java.io.*; class B{ public static String s; } class C1 extends B {}; class C2 extends B {}; class Foo { public static void main (String[] args) throws java.lang.Exception { C1.s = "c1"; C2.s = "c2"; System.out.printf( "C1.s=%s C2.s=%s\n", C1.s, C2.s ); //=> C1.s=c2 C2.s=c2 } } ここは ruby と同じく、クラス変数の持ち主は、その変数が定義されたクラス。 Generics を使うと Generics を使って、型引数を変えてみると java import java.io.*; class B<T>{ public static String s; T dummy; } class C1 extends B<Integer> {}; class C2 extends B<Long> {}; class Foo { public static void main (String[] args) throws java.lang.Exception { C1.s = "c1"; C2.s = "c2"; System.out.printf( "C1.s=%s C2.s=%s\n", C1.s, C2.s ); //=> C1.s=c2 C2.s=c2 } } 異なる型引数でもクラス変数は同じだということがわかる。 なので、型引数の型を static 変数には使えない。なるほどこれが型消去かという感じ。 C++ の場合 テンプレートなし まずはテンプレートなしで。 c++17 #include <iostream> #include <string> struct B{ static inline std::string s; }; struct C1 : public B{}; struct C2 : public B{}; int main(){ C1{}.s = "c1"; C2{}.s = "c2"; std::cout << C1{}.s << " " << C2{}.s << std::endl; //=> c2 c2 return 0; } ruby, Java と同様、static変数(クラス変数) の持ち主は、その変数が定義されたクラス。 テンプレートあり テンプレートクラスの場合。 c++17 #include <iostream> #include <string> template<int n> struct B{ static inline std::string s; }; struct C1 : public B<0>{}; struct C2 : public B<1>{}; int main(){ C1{}.s = "c1"; C2{}.s = "c2"; std::cout << C1{}.s << " " << C2{}.s << std::endl; //=> c1 c2 return 0; } テンプレート引数が異なるクラスは別クラスになる。なので、Java と違って static 変数の型にテンプレート引数が使える。 C# の場合 Generics なし C# using System; class B{ public static String s; } class C1 : B{}; class C2 : B{}; public class Test { public static void Main() { C1.s = "c1"; C2.s = "c2"; Console.WriteLine( "C1.s={0} C2.s={1}", C1.s, C2.s); //=> C1.s=c2 C2.s=c2 } } まあそうだよね。Java と同じ。 Generics あり Generics ありだと C# using System; class B<T>{ public static String s;} class C1 : B<int>{}; class C2 : B<long>{}; public class Test { public static void Main() { C1.s = "c1"; C2.s = "c2"; Console.WriteLine( "C1.s={0} C2.s={1}", C1.s, C2.s); //=> C1.s=c1 C2.s=c2 } } Java とは異なる。 まとめ ひとくちに「クラス変数」というけれど、言語によって意味が違うので要注意。 Python はたぶん異端で、明示されているレシーバがそのクラス変数の持ち主。基底クラス内の cls.class_var は、 cls が異なれば異なる変数。cls 指すクラスの別のスーパークラスで同名のクラス変数を使うと同じ変数となる。 ruby, Java, C++, C# は、クラス変数が定義されたクラスがクラス変数の持ち主。 Generics / template を使う場合。 型引数が違っても同じクラス、というのが Java の立場。 template 引数 / 型引数が違うクラスは別のクラス、というのが C++ と C# の立場。 他に試すべき言語あるかなぁ。 余談 今は亡き J# でジェネリクス使ったらどうなるんだろ。 そもそも J# でジェネリクス使えるかどうかも知らないけど。 以下、投稿の翌日に追加。 Python と ruby のクラス変数の気持ち悪いところ コメントいただいたり他に試したりして、何が気持ち悪いのかわかってきた。 Python の場合 シンプルな例 まずは普通っぽい例。 Python3 class B:v="B0" class C(B):... class D(B):... # [1] print(B.v, C.v, D.v) #=> "B0 B0 B0" # [2] C.v = "C0" print(B.v, C.v, D.v) #=> "B0 C0 B0" [1] の print 内の C.v は、 B の v のことで、 [2] の C.v="C0" の C.v は、 C の v のことなので、C.v="C0" は D.v で得られる値には影響を与えない。 同じ C.v という式だけど、参照するときと代入の左辺になるときで意味が違うのがわかりにくい。とはいえ、オブジェクト指向とはそのようなものだとも思う。 わかりにくい現象 このわかりにくさと Python の += なんかのわかりにくさが合体して、こんなわかりにくいことが起こる。 Python3 class B: a=["a"] b=["b"] c = "c" d = "d" class C(B):... print(repr([B.a, B.b, B.c, B.d])) #=> [['a'], ['b'], 'c', 'd'] C.a += ["+="] C.b = C.b + ["+"] C.c += "+=" C.d = C.d + "+" print(repr([B.a, B.b, B.c, B.d])) #> [['a', '+='], ['b'], 'c', 'd'] リストの += は新たなオブジェクトは作られないので基底クラスの B.a が書き換わり。 文字列の += は新たなオブジェクトが作られるので代入と同じことになるから B.c は書き換わらない。 かなり気持ち悪い動作だと思うけど、なんでそうなるのかは説明できる感じ。 ruby の場合 シンプルな例 一方 ruby の普通っぽい例を実行すると。 ruby class B def set_vb(x); @@v=x; end def vb(); @@v; end end class C < B def set_vc(x); @@v=x; end def vc(); @@v; end end class D < B def vd(); @@v; end end B.new.set_vb("B0") p [B.new.vb, C.new.vc, D.new.vd] #=> ["B0", "B0", "B0"] C.new.set_vc("C0") p [B.new.vb, C.new.vc, D.new.vd] #=> ["C0", "C0", "C0"] 上記の通り Python とは違う。 class C 内の @@v を書き換えると class B 内で定義された @@v が書き換わる。なので、C の派生クラスでも基底スラスでもない D から見える @@v にも影響を与える。 ここだけ見るとそれはそれでありかなと思うけれど。 わかりにくい現象 ならば、派生で @@v を作ってから基底で @@v に代入すると ruby class B def set_vb(x); @@v=x; end def vb(); (defined? @@v) ? @@v : :undefined; end end class C < B def set_vc(x); @@v=x; end def vc(); (defined? @@v) ? @@v : :undefined; end end # [1] C.new.set_vc("C0") p [B.new.vb, C.new.vc] #=> [:undefined, "C0"] # [2] B.new.set_vb("B0") p [B.new.vb, C.new.vc] # ruby 2.7.5 では、["B0", "B0"] # ruby 3.1.0 では例外。class variable @@v of C is overtaken by B なんと、 ruby 3.1 では例外。 2.7.5 では、基底クラスの @@v を書き換えると派生クラスの @@v が書き換わる。難しい。難しすぎるので例外にしたんだろうと思う。 あるいは。 以下を実行すると。 ruby module B @@x = :bx def self.b() [ (defined? @@x) ? @@x : :undef, (defined? @@y) ? @@y : :undef, (defined? @@z) ? @@z : :undef] end end module C @@y = :cy def self.c() [ (defined? @@x) ? @@x : :undef, (defined? @@y) ? @@y : :undef, (defined? @@z) ? @@z : :undef] end end class D include B, C @@x = :dx @@y = :dy @@z = :dz def self.d(); [ (defined? @@x) ? @@x : :undef, (defined? @@y) ? @@y : :undef, (defined? @@z) ? @@z : :undef] end end p [B.b, C.c, D.d] #=> [[:dx, :undef, :undef], [:undef, :dy, :undef], [:dx, :dy, :dz]] module B @@x = :bx2 @@y = :by2 @@z = :bz2 end p [B.b, C.c, D.d] # 3.0.3 だとここで例外 #=> 3.1.0: [[:bx2, :by2, :bz2], [:undef, :dy, :undef], [:bx2, :dy, :dz]] #=> 2.7.5: [[:bx2, :by2, :bz2], [:undef, :dy, :undef], [:bx2, :dy, :bz2]] module C @@x = :cx2 @@y = :cy2 @@z = :cz2 end p [B.b, C.c, D.d] #=> 3.1.0: [[:bx2, :by2, :bz2], [:cx2, :cy2, :cz2], [:bx2, :cy2, :dz]] #=> 2.7.5: [[:bx2, :by2, :bz2], [:cx2, :cy2, :cz2], [:cx2, :cy2, :cz2]] なななんと、2.7.5 でも 3.1.0 でも例外にならずに完走するが、結果が違う。 びっくりした。 3.1.0 だと完走するのに、3.0.3 だと例外。さらにびっくりした。 びっくりしたし、それぞれどんな理路でそのような出力になったり例外になったりするのかは私には説明できない。 2.7.5 と 3.1.0 で結果が違うことに気づいた時点で真剣に考えるのを放棄したということでもあるけれど。 再度まとめ Python と ruby のクラス変数は、両方ともわりと思いがけないことが起こる。 どちらも気持ち悪いけど、どちらかというとというかわりと圧倒的に ruby の方が気持ち悪い。 ruby のクラス変数はめちゃくちゃ気持ち悪いし、中の人も悩んでいるんだと思うので、継承が絡む局面での利用は避けたほうがいいと思う。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む