HuaTham
HuaTham

Reputation: 7596

Read-only access for instance property in Swift

I am aware of private(set) as discussed here and here but it doesn't seem to work for my situation where I have instance variable with internal properties.

I have an instance property where I would like it to have read-only access. The following code shows some concrete example.

class Point {
    var x: Int
    var y: Int

    init(_ x: Int, _ y: Int) {
        self.x = x
        self.y = y
    }
}

class Sprite {
    private(set) var origin = Point(0,0)

    func incrementX() {
        origin.x += 1
    }

    func incrementY() {
        origin.y += 1
    }
}

Here, my intent is to make Sprite the sole owner of origin, allowing it to be the only one who can modify it, otherwise making origin read-only to the public. However:

var sprite = Sprite()
sprite.origin = Point(100, 200)        // Error: setter is inaccessibie. Expected behavior.
sprite.origin.x = 150                  // No error. The origin.x will be set to 150.

Apparently I can still modify origin by modifying internal x and y directly, but this is not my intent.

How do I make origin truly read-only? Am I missing something?

Modifying Point to private var x: Int or fileprivate var x: Int won't work for me because I want to be able to let an external class like Sprite modify Point.

Edit: From comment below, this is not my actual scenario, since someone comments that I should be using CGPoint. The code serves as as example to make my question more concrete. I actually have a complex class as instance property in another class, not this simple Point. I also can't use it as struct due to other design constraints.

Edit2: An answer below points out that you cannot prevent the reference from being mutable. That's a limitation in Swift I just learned, because apparently, I can achieve exactly what I wanted in C++ using Point const &:

class Point {
public:
    int x;
    int y;

    Point(int _x, int _y) {
        x = _x;
        y = _y;
    }

    Point & operator=(const Point & rhs) {
        x = rhs.x;
        y = rhs.y;
        return *this;
    }

    ...

};

class Sprite {

    Point origin;

public:

    Sprite()
    : origin(0,0)
    {}

    // HERE
    Point const & getOrigin() const { return origin; }

    void incrementX() {    origin.x += 1; }
    void incrementY() {    origin.y += 1; }

};

int main(int argc, const char * argv[]) {
    Sprite sprite = Sprite();
    Point const & o = sprite.getOrigin();

    cout << "o: " << o << endl;  // (0,0)
//    o.x = 10;                  // Error: Cannot assign to const-qualified type.
//    o = Point (100, 200);      // Error: no viable overlaod =.


    sprite.incrementX();

    cout << "o: " << o << endl;  // (1,0). Swift can't get this behavior with `struct Point`, but it won't prevent some writing attempts with `class Point`.
    cout << "sprite.origin: " << sprite.getOrigin() << endl;  // (1,0).

    return 0;
}

Upvotes: 3

Views: 3197

Answers (5)

kevin
kevin

Reputation: 2071

It is impossible to get your desired result by exposing Point directly, it is a class and will be passed by reference. You can use a protocol to only expose the parts of Point you desire.

protocol ReadablePoint {
    var x: Int { get }
    var y: Int { get }
}

class Point: ReadablePoint {
    var x: Int
    var y: Int

    init(_ x: Int, _ y: Int) {
        self.x = x
        self.y = y
    }
}

class Sprite {
    private var _origin = Point(0,0)
    var origin: ReadablePoint { return _origin }

    func incrementX() {
        _origin.x += 1
    }

    func incrementY() {
        _origin.y += 1
    }
}

Which results in:

var sprite = Sprite()
sprite.origin = Point(100, 200)        // error: cannot assign to property: 'origin' is a get-only property
sprite.origin.x = 150                  // error: cannot assign to property: 'x' is a get-only property

Upvotes: 1

Tiziano Bruschetta
Tiziano Bruschetta

Reputation: 759

class Point {
 private var x: Int
 private var y: Int

 init(_ x: Int, _ y: Int) {
     self.x = x
     self.y = y
 }

 func incrementX() {
     x += 1
 }

 func incrementY() {
     y += 1
 }

 func setX(_ value : Int){
     x = value
 }

 func setY(_ value : Int){
     y = value
 }
}

class Sprite {

    private (set) var origin = Point(0,0)

}

if you need to get the x and y value you should add getter in your Point class like this:

func getX()->Int{
    return x
}

so you can try this:

let sprite = Sprite()
sprite.origin.incrementX()

let point = Point(10,20)
point.setX(12)
point.setY(100)

so you can have origin read-only, in my opinion you have to set x and y private.

Upvotes: 1

matt
matt

Reputation: 534885

Make origin completely private. The only public thing should be a read-only computed property. Your public incrementX and incrementY methods will still work as desired, because they are allowed to access origin.

If Point is a struct, then it is sufficient for this read-only property to return origin, because it will be a copy:

var currentOrigin : Point {
    return self.origin
}

But if you insist on making Point a class rather than a struct, then you are vending a reference to the same Point instance you are already retaining, and you cannot prevent it from being mutable. Therefore you will have to produce a copy of self.origin yourself, so that you are not vending a mutable reference to your own private instance:

var currentOrigin : Point {
    return Point(self.origin.x,self.origin.y)
}

(The way Objective-C Cocoa typically solves this sort of problem is by implementing a class cluster of immutable/mutable pairs; for example, we maintain an NSMutableString but we vend an NSString copy, so that our mutable string cannot be mutated behind our back. However, I think you are being very foolish to reject the use a struct, since this is one of Swift's huge advantages over Objective-C, i.e. it solves the very problem you are having.)

Upvotes: 2

BEN MESSAOUD Mahmoud
BEN MESSAOUD Mahmoud

Reputation: 736

try this :

class Point {
var x: Int
var y: Int

init(_ x: Int, _ y: Int) {
    self.x = x
    self.y = y
}
}

class Sprite {
private var privatePoint = Point(0,0)
var origin:Point{
    get{
        return privatePoint
    }
}
}

so you can do this :

var sprite = Sprite()
sprite.origin = Point(100, 200)        // Error: setter is inaccessibie. Expected behavior.
sprite.origin.x = 150                  // No error. The origin.x will be set to 150.

Upvotes: 0

Luca Angeletti
Luca Angeletti

Reputation: 59496

Just make Point a struct

struct Point {
    var x: Int
    var y: Int

    init(_ x: Int, _ y: Int) {
        self.x = x
        self.y = y
    }
}

class Sprite {
    private(set) var origin = Point(0,0)

    func incrementX() {
        origin.x += 1
    }

    func incrementY() {
        origin.y += 1
    }
}

And try again

var sprite = Sprite()
sprite.origin.x = 1
error: cannot assign to property: 'origin' setter is inaccessible

BTW: Why don't you simply use CGPoint instead of defining a new type?

Upvotes: 4

Related Questions