Chapter 6 Methods
Exercise 6.1: Implement these additional methods:
func (*IntSet) Len() int // return the number of elements
func (*IntSet) Remove(x int) // remove x from the set
func (*IntSet) Clear() // remove all elements from the set
func (*IntSet) Copy() *IntSet // return a copy of the set
// An IntSet is a set of small non-negative integers.
// Its zero value represents the empty set.
type IntSet struct {
words []uint
count int
}
// return the number of elements
func (s *IntSet) Len() int {
return s.count
}
// remove x from the set
func (s *IntSet) Remove(x int) {
if s.Has(x) {
word, bit := x/size, uint(x%size)
s.words[word] &^= 1 << bit
}
}
// remove all elements from the set
func (s *IntSet) Clear() {
s.words = nil
s.count = 0
}
// return a copy of the set
func (s *IntSet) Copy() *IntSet {
c := NewIntSet()
c.words = make([]uint, len(s.words))
copy(c.words, s.words)
c.count = s.count
return c
}
Exercise 6.2: Define a variadic (*IntSet).AddAll(...int)
method that allows a list of values to be added, such as s.AddAll(1, 2, 3)
.
func (s *IntSet) AddAll(x ...int) {
for _, i := range x {
s.Add(i)
}
}
Exercise 6.3: (*IntSet).UnionWith
computes the union of two sets using |, the word-parallel bitwise OR operator. Implement methods for IntersectWith, DifferenceWith, and Symmetric Difference
for the corresponding set operations. (The symmetric difference of two sets contains the elements present in one set or the other but not both.)
// IntersectWith return intersection of s and t.
func (s *IntSet) IntersectWith(t *IntSet) *IntSet {
r := NewIntSet()
for _, i := range t.Elems() {
if s.Has(i) {
r.Add(i)
}
}
return r
}
// DifferenceWIth return difference of s and t.
func (s *IntSet) DifferenceWith(t *IntSet) *IntSet {
r := NewIntSet()
for _, i := range s.Elems() {
if !t.Has(i) {
r.Add(i)
}
}
return r
}
// SymmetricDifference return symmetric difference of s and t.
func (s *IntSet) SymmtericDifference(t *IntSet) *IntSet {
u := s.Copy()
u.UnionWith(t)
return u.DifferenceWith(s.IntersectWith(t))
}
Exercise 6.4: Add a method Elems
that returns a slice containing the elements of the set, suitable for iterating over with a range loop.
func (s *IntSet) Elems() []int {
if s.Len() == 0 {
return nil
}
r := make([]int, 0, s.Len())
for i, word := range s.words {
if word == 0 {
continue
}
for j := 0; j < size; j++ {
if word&(1<<uint(j)) != 0 {
r = append(r, size*i+j)
}
}
}
return r
}
Exercise 6.5: The type of each word used by IntSet is uint64, but 64-bit arithmetic may be inefficient on a 32-bit platform. Modify the program to use the uint type, which is the most efficient unsigned integer type for the platform. Instead of dividing by 64, define a constant holding the effective size of uint in bits, 32 or 64. You can use the perhaps too-clever expression 32 << (^uint(0) >> 63)
for this purpose.
const size = 32 << (^uint(0) >> 63)
// replace 64 with size
Notes:
OO: object is simply a value or variable that has methods, and a method is a function associated with a particular type.
OOP: An object-oriented program is one that uses methods to express the properties and operations of each data structure so that clients need not access the object’s representation directly.
Method: A method is declared with a variant of the ordinary function declaration in which an extra parameter appears before the function name. The parameter attaches the function to the type of that parameter. The extra parameterpis called the method’s receiver, a legacy from early object-oriented languages that described calling a method as ‘‘sending a message to an object.’’
Unique in GO: In allowing methods to be associated with any type, Go is unlike many other object-oriented languages. It is often convenient to define additional behaviors for simple types such as numbers, strings, slices, maps, and sometimes even functions. Methods may be declared on any named type defined in the same package, so long as its underlying type is neither a pointer nor an interface.
Convension: In a realistic program, convention dictates that if any method of Point has a pointer receiver, then all methods of Point should have a pointer receiver, even ones that don’t strictly need it.
Attention: Named types (Point) and pointers to them (*Point) are the only types that may appear in a receiver declaration. Furthermore, to avoid ambiguities, method declarations are not permitted on named types that are themselves pointer types:
type P *int func (P) f() { / ... / } // compile error: invalid receiver type
In every valid method call expression, exactly one of these three statements is true
- Either the receiver argument has the same type as the receiver parameter.
- Or the receiver argument is a variable of type T and the receiver parameter has type *T. The compiler implicitly takes the address of the variable.
- Or the receiver argument has type *T and the receiver parameter has type T. The compiler implicitly dereferences the receiver, in other words, loads the value.
If all the methods of a named typeThave a receiver type ofTitself (not*T), it is safe to copy instances of that type; calling any of its methods necessarily makes a copy. For example, time.Duration values are liberally copied, including as arguments to functions. But if any method has a pointer receiver, you should avoid copying instances of T because doing so may violate internal invariants. For example, copying an instance of bytes.Buffer would cause the original and the copy to alias (§2.3.2) the same underlying array of bytes. Subsequent method calls would have unpredictable effects.
Nil is a valid Receiver value
Resolves: When the compiler resolves a selector such as p.ScaleBy to a method, it first looks for a directly declared method named ScaleBy, then for methods promoted once from ColoredPoint’s embedded fields, then for methods promoted twice from embedded fields within PointandRGBA, and so on. The compiler reports an error if the selector was ambiguous because two methods were promoted from the same rank.
Method Value: The selector p.Distance yields a method value, a function that binds a method (Point.Distance) to a specific receiver value p.
Method Expression: A method expression, written
T.f or (*T).f
where T is a type, yields a function value with a regular first parameter taking the place of the receiver, so it can be called in the usual way.Encapsulation: Go has only one mechanism to control the visibility of names: capitalized identifiers are exported from the package in which they are defined, and uncapitalized names are not. The same mechanism that limits access to members of a package also limits access to the fields of a struct or the methods of a type. As a consequence, to encapsulate an object, we must make it a struct.
Another consequence of this name-based mechanism is that the unit of encapsulation is the package, not the type as in many other languages. The fields of a struct type are visible to all code within the same package. Whether the code appears in a function or a method makes no difference.
Encapsulation is not always desirable.