Ruby: prevent updating an array, which is a class variable

I am writing a simple Tic Tac Toe game in which I have a class for Board, Player, Computer and the game itself. In my Board class, I set the @board class variable (which is an array) as attr_reader, which should prevent direct access to it. Although the following will not work (as intended)

game_board = Board.new game_board.board = "some junk" 

The following works, which I do not want to do

 game_board = Board.new game_board.board[0] = "some junk" 

How do I prevent a variable from writing to the @board class array? The current class code is below:

 class Board attr_reader :board def initialize create_board end private def create_board @board = Array.new(3).map{Array.new(3)} end end game_board = Board.new game_board.board #=> [[nil,nil,nil],[nil,nil,nil],[nil,nil,nil]] game_board.board = "junk" #=> undefined method 'board =' game_board.board[0] = "junk" game_board.board #=> ["junk",[nil,nil,nil],[nil,nil,nil]] #I don't want to allow this! 

I tried searching for this, but to no avail, but I am a complete newbie, so I cannot use the correct search terms

+8
arrays ruby class
source share
2 answers

I believe you need to make the array immutable.

You can use Array # freeze to achieve this.

Your code after that should look like this:

 class Board attr_reader :board def initialize create_board end private def create_board @board = Array.new(3).map{Array.new(3).freeze}.freeze end end 

When running your first example:

 >> game_board = Board.new #<Board:0x00000001648b50 @board=[[nil, nil, nil], [nil, nil, nil], [nil, nil, nil]]> >> game_board.board = "some junk" NoMethodError: undefined method `board=' for #<Board:0x00000001648b50> from (irb):14 from /home/alfie/.rvm/rubies/ruby-2.1.3/bin/irb:11:in `<main>' 

When running the second example:

 >> game_board = Board.new #<Board:0x00000001639e48 @board=[[nil, nil, nil], [nil, nil, nil], [nil, nil, nil]]> >> game_board.board[0] = "some junk" RuntimeError: can't modify frozen Array from (irb):16 from /home/alfie/.rvm/rubies/ruby-2.1.3/bin/irb:11:in `<main>' 
+7
source share

Only the attr_reader definition, without the attr_writer , will only prevent the @board variable from being @board . In other words, your Board class does not provide an interface for modifying what is stored in @board , but does nothing to prevent the initial value from being changed.

You can use freeze :

 def create_board @board = Array.new(3) { Array.new(3).freeze } @board.freeze end 

(also you do not need map )

Freezing a top-level array and nested ones will do what you describe, but I think it will also break your game, because modifications will be completely impossible.

I would suggest not exposing @board at all and consider it closed. Then you must set the interface to set the values ​​on the board and provide a method for returning the view of the board displayed for reading.

 class Board def initialize create_board end def []=(x, y, value) @board[x][y] = value end def board @board.map { |a| a.dup.freeze }.freeze end private def create_board @board = Array.new(3) { Array.new(3) } end end b = Board.new b.board # => [[nil, nil, nil], [nil, nil, nil], [nil, nil, nil]] b[1,2] = "x" b[0,0] = "o" b.board # => [["o", nil, nil], [nil, nil, "x"], [nil, nil, nil]] b.board[0] = "junk" # RuntimeError: can't modify frozen Array b.board[0][1] = "junk" # RuntimeError: can't modify frozen Array b.board # => [["o", nil, nil], [nil, nil, "x"], [nil, nil, nil]] 
+6
source share

All Articles