검색결과 리스트
글
3. 기본문법3(Containers, Blocks, and Iterators)
1. Containers
DATA를 저장하는 Container객체에 대해 알아보자
이런저런 설명 빼고 어떻게 사용하는지만 보면 될 듯 싶다.
- Arrays
a = [ 3.14159, "pie", 99 ]
a.type » Array
a.length » 3
a[0] » 3.14159
a[1] » "pie"
a[2] » 99
a[3] » nil
b = Array.new
b.type » Array
b.length » 0
b[0] = "second"
b[1] = "array"
b » ["second", "array"]
이건 그냥 대략 이해 가고
a = [ 1, 3, 5, 7, 9 ]
a[-1] » 9
a[-2] » 7
a[-99] » nil
여기서 주의할 점은 -(마이너스)인덱스도 거꾸로 1까지만 가고 그 이상이 되면(-6) nil값을 return한다.
a = [ 1, 3, 5, 7, 9 ]
a[1, 3] » [3, 5, 7]
a[3, 1] » [7]
a[-3, 2] » [5, 7]
이것은 arrya[start, count] 방식으로 사용한다.
a = [ 1, 3, 5, 7, 9 ]
a[1..3] » [3, 5, 7]
a[1...3] » [3, 5]
a[3..3] » [7]
a[-3..-1] » [5, 7, 9]
.. 은 마지막것을 포함 ...은 마지막을 제외한다.
a = [ 1, 3, 5, 7, 9 ] » [1, 3, 5, 7, 9]
a[1] = 'bat' » [1, "bat", 5, 7, 9]
a[-3] = 'cat' » [1, "bat", "cat", 7, 9]
a[3] = [ 9, 8 ] » [1, "bat", "cat", [9, 8], 9]
a[6] = 99 » [1, "bat", "cat", [9, 8], 9, nil, 99]
마지막에 인덱스를 건더뛰게되면 중간에 빈것은 nil
a = [ 1, 3, 5, 7, 9 ] » [1, 3, 5, 7, 9]
a[2, 2] = 'cat' » [1, 3, "cat", 9]
a[2, 0] = 'dog' » [1, 3, "dog", "cat", 9]
a[1, 1] = [ 9, 8, 7 ] » [1, 9, 8, 7, "dog", "cat", 9]
a[0..3] = [] » ["dog", "cat", 9]
a[5] = 99 » ["dog", "cat", 9, nil, nil, 99]
여기서는 처음것은 index 두번째것은 length(0은 빈곳 삽입, 1은 한개만 replace, 2는 두개 replace)
- Hashes
h = { 'dog' => 'canine', 'cat' => 'feline', 'donkey' => 'asinine' }
h.length » 3
h['dog'] » "canine"
h['cow'] = 'bovine'
h[12] = 'dodecine'
h['cat'] = 99
h » {"cow"=>"bovine", "cat"=>99, 12=>"dodecine", "donkey"=>"asinine", "dog"=>"canine"}
2. Implementing a SongList Container
class Song
attr_reader :name, :artist, :duration
def initialize(name, artist, duration)
@name = name
@artist = artist
@duration = duration
end
def name
@name
end
def artist
@artist
end
def duration
@duration
end
def duration=(newDuration)
@duration = newDuration
end
def durationInMinutes
@duration/60.0
end
end
class SongList
def initialize
@songs = Array.new
end
def append(aSong)
@songs.push(aSong)
self
end
def deleteFirst
@songs.shift
end
def deleteLast
@songs.pop
end
def [] (key)
if key.kind_of?(Integer)
@songs[key]
else
# ...
end
end
end
list = SongList.new
list.
append(Song.new('title1','artist1',1)).
append(Song.new('title2','artist2',2)).
append(Song.new('title3','artist3',3)).
append(Song.new('title4','artist4',4))
#puts list.deleteFirst
#puts list.deleteFirst
#puts list.deleteFirst
#puts list.deleteFirst
#puts list.deleteFirst
#puts list[0]
#puts list[2]
#puts list[9]
여기서 shift 는 queue라 봤을때 처음값을 삭제
pop은 마지막것을 삭제한다 보면 된다.
key.kind_of?(Integer) 부분은 인자값을로 받은것이 정수인지 구분
3. Blocks and Iterators
위의것은 인덱스로만 검색하는 것이고 이제부터는 문자열로 검색 가능토록 만들어보자
총 3가지 방법이 있다.
첫번째는
class SongList
def [](key)
if key.kind_of?(Integer)
return @songs[key]
else
for i in 0...@songs.length
return @songs[i] if key == @songs[i].name
end
end
return nil
end
end
다른 언어에서와 같이 평범하다. 여기에 루비 문법을 적용해보면
두번째와 같다.
class SongList
def [](key)
if key.kind_of?(Integer)
result = @songs[key]
else
result = @songs.find { |aSong| key == aSong.name }
end
return result
end
end
어랏 간단해졌네... 하지만 이것도 맛배기.. 더 간단히 줄여보자
세번째는 다음과 같다.
class SongList
def [](key)
return @songs[key] if key.kind_of?(Integer)
return @songs.find { |aSong| aSong.name == key }
end
end
헐;;; 사람들이 루비 칭찬하는 이유를 알겠다.....
이제부터 나는 세번째 방법으로 할련다.
- Implementing Iterator
앞서 아래와 같이 yield의 간단한 사용법을 배웠다
def threeTimes
yield
yield
yield
end
threeTimes { puts "Hello" }
produces: Hello
Hello
Hello
이것 응용한 것을 보면 다음과 같다.(아 머리 아파 하지 말자)
def fibUpTo(max)
i1, i2 = 1, 1 # parallel assignment
while i1 <= max
yield i1
i1, i2 = i2, i1+i2
end
end
fibUpTo(1000) { |f| print f, " " }
produces: 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
값의 흐름을 보면 간단하니깐
i1 i2
1 1
1 2
2 3
3 5
5 8
....
610 987
이런 형태로 값이 쌓인다.
주의해서 볼 것은 병렬로 값의 저장이 가능하다는 것과
yield로 자기를 호출한 것에 다시 값을 return 해 준다는 것.. 쫌만 쬐려보면 금방 이해 된다.
더 응용된 예제를 보자
class Array
def find
for i in 0...size
value = self[i]
return value if yield(value)
end
return nil
end
end
[1, 3, 5, 7, 9].find {|v| v*v > 30 }
위 예제는 find를 하면서 loop를 돌고
일방적으로 값을 넘기는 것이 아니고 서로 통신하고 잇따 보면 된다. 결과 값을 puts로 찍으면 7
설명이 길어지니 pass
또다른 형태의 iterator 방법을 보자
[ 1, 3, 5 ].each { |i| puts i }
produces: 1
3
5
각각의 값을 받고 1, 3, 5출력
puts ["H", "A", "L"].collect { |x| x.succ }
이것은 I B M 출력한다.
succ는 바로 다음 ascii코드를 뿌리는건가? 에랏 모르겠다.
4. Ruby Compared with C++ and Java
class Array
def inject(n)
each { |value| n = yield(n, value) }
n
end
def sum
inject(0) { |n, value| n + value }
end
def product
inject(1) { |n, value| n * value }
end
end
puts [ 1, 2, 3, 4, 5 ].sum #≫ 15
puts [ 1, 2, 3, 4, 5 ].product #≫ 120
이것도 설명이 길어지므로 pass
5. Blocks for Transactions
Blocks can be used to define a chunk of code that must be run under some kind of transactional control. For example, you'll often open a file, do something with its contents, and then want to ensure that the file is closed when you finish. Although you can do this using conventional code, there's an argument for making the file responsible for closing itself. We can do this with blocks. A naive implementation (ignoring error handling) might look something like the following.
class File
def File.openAndProcess(*args)
f = File.open(*args)
yield f
f.close()
end
end
File.openAndProcess("testfile", "r") do |aFile|
print while aFile.gets
end
produces: This is line one
This is line two
This is line three
And so on...
This small example illustrates a number of techniques. The openAndProcess method is a class method---it may be called independent of any particular File object. We want it to take the same arguments as the conventional File.open method, but we don't really care what those arguments are. Instead, we specified the arguments as *args, meaning ``collect the actual parameters passed to the method into an array.'' We then call File.open, passing it *args as a parameter. This expands the array back into individual parameters. The net result is that openAndProcess transparently passes whatever parameters it received to File.open .
Once the file has been opened, openAndProcess calls yield, passing the open file object to the block. When the block returns, the file is closed. In this way, the responsibility for closing an open file has been passed from the user of file objects back to the files themselves.
Finally, this example uses do...end to define a block. The only difference between this notation and using braces to define blocks is precedence: do...end binds lower than ``{...}''. We discuss the impact of this on page 234.
The technique of having files manage their own lifecycle is so useful that the class File supplied with Ruby supports it directly. If File.open has an associated block, then that block will be invoked with a file object, and the file will be closed when the block terminates. This is interesting, as it means that File.open has two different behaviors: when called with a block, it executes the block and closes the file. When called without a block, it returns the file object. This is made possible by the method Kernel::block_given? , which returns true if a block is associated with the current method. Using it, you could implement File.open (again, ignoring error handling) using something like the following.
class File
def File.myOpen(*args)
aFile = File.new(*args)
# If there's a block, pass in the file and close
# the file when it returns
if block_given?
yield aFile
aFile.close
aFile = nil
end
return aFile
end
end
6. Blocks Can Be Closures
Let's get back to our jukebox for a moment (remember the jukebox?). At some point we'll be working on the code that handles the user interface---the buttons that people press to select songs and control the jukebox. We'll need to associate actions with those buttons: press STOP and the music stops. It turns out that Ruby's blocks are a convenient way to do this. Let's start out by assuming that the people who made the hardware implemented a Ruby extension that gives us a basic button class. (We talk about extending Ruby beginning on page 169.)
bStart = Button.new("Start")
bPause = Button.new("Pause")
# ...
What happens when the user presses one of our buttons? In the Button class, the hardware folks rigged things so that a callback method, buttonPressed, will be invoked. The obvious way of adding functionality to these buttons is to create subclasses of Button and have each subclass implement its own buttonPressed method.
class StartButton < Button
def initialize
super("Start") # invoke Button's initialize
end
def buttonPressed
# do start actions...
end
end
bStart = StartButton.new
There are two problems here. First, this will lead to a large number of subclasses. If the interface to Button changes, this could involve us in a lot of maintenance. Second, the actions performed when a button is pressed are expressed at the wrong level; they are not a feature of the button, but are a feature of the jukebox that uses the buttons. We can fix both of these problems using blocks.
class JukeboxButton < Button
def initialize(label, &action)
super(label)
@action = action
end
def buttonPressed
@action.call(self)
end
end
bStart = JukeboxButton.new("Start") { songList.start }
bPause = JukeboxButton.new("Pause") { songList.pause }
The key to all this is the second parameter to JukeboxButton#initialize. If the last parameter in a method definition is prefixed with an ampersand (such as &action), Ruby looks for a code block whenever that method is called. That code block is converted to an object of class Proc and assigned to the parameter. You can then treat the parameter as any other variable. In our example, we assigned it to the instance variable @action. When the callback method buttonPressed is invoked, we use the Proc#call method on that object to invoke the block.
So what exactly do we have when we create a Proc object? The interesting thing is that it's more than just a chunk of code. Associated with a block (and hence a Proc object) is all the context in which the block was defined: the value of self, and the methods, variables, and constants in scope. Part of the magic of Ruby is that the block can still use all this original scope information even if the environment in which it was defined would otherwise have disappeared. In other languages, this facility is called a closure.
Let's look at a contrived example. This example uses the method proc, which converts a block to a Proc object.
def nTimes(aThing)
return proc { |n| aThing * n }
end
p1 = nTimes(23)
p1.call(3) » 69
p1.call(4) » 92
p2 = nTimes("Hello ")
p2.call(3) » "Hello Hello Hello "
The method nTimes returns a Proc object that references the method's parameter, aThing. Even though that parameter is out of scope by the time the block is called, the parameter remains accessible to the block