Rubyのコレクション書き換え時の注意点

これも嵌りがちな内容。
コレクションのコピーについて。

Effective Ruby

Effective Ruby

項目16 コレクションを書き換える前に引数として渡すコレクションのコピーを作っておこう

ラジオのTunerを例に。

class Tuner
  def initialize(presets)
    @presets = presets
    clean
  end

  private
    def clean
      # 末尾が奇数のみを抽出
      @presets.delete_if { |f| f[-1].to_i.even? }
    end
end

>> presets = %w(90.1 106.2 88.5)
>> tuner = Tuner.new(presets)

# 書き換えられちゃった!
>> presets
=> ["90.1", "88.5"]

改善案(reject

Array#delete_if でなく Array#reject を使えば良い。
でも、どっかで書き換えられるかもしれない。

改善案(clone/dup

コピーすれば良い。
cloneはオブジェクトの状態(freezeと特メソッド)を残す。
dupは残さない。
大抵の場合はdupで良い。

class Tuner
  def initialize(presets)
    @presets = presets.dup
    clean
  end
end

コピー時の注意点

dup/cloneはシャロー(shallow)コピーを返す。
コレクションクラスの場合、コンテナのコピーは作られるが、要素のコピーは作られない。

>> a = ["Polar"]
>> b = a.dup << "Bear"
=> ["Polar", "Bear"]

>> b.each { |x| x.sub!("lar", "oh") }
=> ["Pooh", "Bear"]

# 書き換えられてる
>> a
=> ["Pooh"]

deepコピーが欲しい場合

Marashalを使えば手軽にできるが、メモリも食うし、Marshalみ対応していないオブジェクト(IO、Fileなど)もあるので要注意。

>> a = ["Polar"]
>> b = Marshal.load(Marshal.dump(a)) << "Bear"
=> ["Polar", "Bear"]

>> b.each { |x| x.sub!("lar", "oh") }
=> ["Pooh", "Bear"]

# 書き換えられていない!
>> a
=> ["Polar"]

覚えておくべき事項

  • Rubyのメソッド引数は値渡しではなく参照渡しである。ただし、この規則には、Fixnumオブジェクトという顕著な例外がある。
  • 引数として渡されたコレクションは、書き換える前にコピーを作ろう。
  • dup、cloneメソッドは、シャローコピーしか作らない。
  • ほとんどのオブジェクトでは、Marshalを使えば必要な時にディープコピーを作れる。

クラス変数("@@")やめとけってよ

クラス変数("@@")使う人あんまりいないと思うけど、グローバル変数と同じようなもんだから使うなよっていう話。

Effective Ruby

Effective Ruby

項目15 クラス変数よりもクラスインスタンス変数を使うようにしよう

クラス変数を使ってSingletonを実装してみる

一見いい感じ。

class Singleton
  private_class_method(:new, :dup, :clone)

  def self.instance
    @@single ||= new
  end
end

でもサブクラスだとNG

全てのサブクラス間で共有されてしまう。

class Configuration < Singleton; end
class Database < Singleton; end

>> Configuration.instance
#<Configuration:0x00007fed1a890c88>

# NG: Configurationになっちゃう
>> Database.instance
#<Configuration:0x00007fed1a890c88>

解決方法:クラスインスタンス変数

「クラスメソッド内でインスタンス変数?」となるかもしれないが、クラスもオブジェクト。

class Singleton
  private_class_method(:new, :dup, :clone)

  def self.instance
    # @@ -> @に変更するだけ
    @single ||= new
  end
end

おまけ

スレッドを考慮すると上記実装では不十分。標準ライブラリのSingletonを使うと良い。

require "singleton"

class Configuration
  include Singleton
end

覚えておくべき事項

  • クラス変数よりもインスタンス変数を使うようにしよう。
  • クラスはオブジェクトなので、専用のプライベートなインスタンス変数セットを持っている。

MacのCPU温度を調べる方法

少し前に古いMacoBook AirからMac Bookに乗り換えたんだけど、Mac本体が結構熱くなってヤキモキしています。
とりあえず、CPU温度を測るツールを導入して見ました。


 

iStats

なんとRubygemsで公開されていました。見た目が良い。
Chris911/iStats: Ruby gem for your mac stats

f:id:rochefort:20180221113432p:plain

ソースをのぞいてみると

iStats/smc.c at master · Chris911/iStats
CのコードでMacAPIを叩いているようです。
なるほど、やっぱそうだよね。

Bitbarプラグイン

iStatsの結果をbitbar を使ってメニューバーに表示させようかと思ったのですが、すでにプラグインがあるのでは?と思って検索してみると以下がありました。
CPU Temperature on BitBar - Put anything in your Mac OS X menu bar
 
なんとこちらは、懐かしのsmcFanControlのバイナリを流用しているようです。

# 'smc' can be downloaded from: http://www.eidac.de/smcfancontrol/smcfancontrol_2_4.zip
# One-liner:
# curl -LO http://www.eidac.de/smcfancontrol/smcfancontrol_2_4.zip && unzip -d temp_dir_smc smcfancontrol_2_4.zip && cp temp_dir_smc/smcFanControl.app/Contents/Resources/smc /usr/local/bin/smc ; rm -rf temp_dir_smc smcfancontrol_2_4.zip

そしてキモい整形をしています。

$ smc -k TC0P -r | sed 's/.*bytes \(.*\))/\1/' |sed 's/\([0-9a-fA-F]*\)/0x\1/g' | perl -ne 'chomp; ($low,$high) = split(/ /); print (((hex($low)*256)+hex($high))/4/64); print "\n";'
59.0625

 
とりあえず、間隔が5sになっていたのを変更し、このプラグインを使っているところです。
f:id:rochefort:20180221113519p:plain

 

余談

App Storeで探して見たら、おしゃれなシステムメトリクス表示ができる Monit というNotificationツールがあったので購入して見ました。$3です。

MONIT

MONIT

  • Tildeslash
  • ユーティリティ
  • ¥360

 
ただ、こちらはCPUの温度計測はできないみたい。ですが、見た目がいいので気に入っています。
f:id:rochefort:20180221113754p:plain