DEV Community

Cover image for Immutable ตัวแปรที่เปลี่ยนค่าไม่ได้!
Ta
Ta

Posted on • Updated on • Originally published at tamemo.com

Immutable ตัวแปรที่เปลี่ยนค่าไม่ได้!

ในบทนี้เราจะได้รู้จักกับศัพท์ 2 คำคือ "Mutable" ซึ่งแปลว่า เปลี่ยนแปลงได้, ไม่แน่นอน, แปรผัน แต่เมื่อมันมาอยู่ในเรื่องการเขียนโปรแกรม มันจะหมายถึงชนิดของตัวแปรที่สามารถเปลี่ยนแปลงค่าได้นั่นเอง

อ่านมาถึงตรงนี้ ถ้าใครเพิ่งหักเขียนโปรแกรมอาจจะมีการ เอ๊ะ!? ขึ้นมาในใจได้ ว่าตัวแปรหรือ variable เนี่ยมันก็ต้องเปลี่ยนค่าได้อยู่แล้วไม่ใช่เหรอยังไง ถ้าเปลี่ยนค่าไม่ได้เราจะคำนวณผลได้ยังไง

ประเด็นก็คือมันมีตัวแปรอีกแบบหนึ่งที่ตรงข้ามกับ mutable นั่นคือตัวแปรประเภท "Immutable" (ภาษาอังกฤษ เติม im- เข้าไปข้ามหน้าส่วนใหญ่จะทำให้ความหมายกลับเป็นตรงกันข้าม) แปลว่าเปลี่ยนแปลงไม่ได้ยังไงล่ะ

ตัวแปรที่เป็น immutable นั้นจะกำหนดค่าได้ครั้งเดียวเท่านั้น หลังจากนั้นตัวแปรจะอยู่ในสถานะ Read-Only ไปจนกว่าตัวแปรนั้นจะถูกทำลาย

ในแต่ละภาษามีอาจจะมีการกำหนดว่าตัวแปรไหนเป็น immutable ที่ต่างกัน เช่น

Language Mutable Immutable
JavaScript let x หรือ var x const x
Java int x final int x
C# int x readonly int x
Kotlin var x val x
Swift var x let x
Dart var x final x

(ใครต้องเขียนหลายภาษาบอกเลยว่ามีงงแน่นอน ฮา)

คำเตือน! ในบางภาษา, ตัวแปร immutalbe นั้นอาจจะถูกแบ่งเป็น 2 ประเภทย่อยอีก นั่นคือ immutable แบบ runtime (ค่าสร้างตอนรันโปรแกรม จะคำนวณมาจากค่าอื่นอีกทีก็ได้) และ compile-time (ค่าสร้างตั้งแต่ตอนคอมไพล์ ต้องเป็นค่า constant เท่านั้น เช่น PI = 3.14) ดังนั้นใช้งานภาษาอะไรอยู่ อ่านdocของภาษานั้นดีๆ ด้วยล่ะ!

ทำไมต้อง Immutable ?

ในมุมมองโปรแกรมเมอร์ทั่วไป เราสามารถเปลี่ยนแปลงค่าตัวแปรยังไงก็ได้

var x = 100
x = x + 1
Enter fullscreen mode Exit fullscreen mode

เช่น สร้าง x มาแล้ว อยากเพิ่มค่าให้ x อีก 1 ก็สั่ง +1 เข้าไป (เคสนี้เจอบ่อยเวลาสร้างลูป ตรง i++ ยังไงล่ะ)

ในเคสนี้ ถ้าเราเปลี่ยนค่าตัวแปรไม่ได้ เราก็ต้องเขียนโค้ดแบบนี้แทน

var x = 100
var y = x + 1

//หรือ
var x = 100
var y = add(x, 1)
Enter fullscreen mode Exit fullscreen mode

ซึ่งดูแล้วหาประโยชน์อะไรไม่ได้เลย! โอเค นี่คงไม่ใช่ตัวอย่างที่ดีนักสำหรับการใช้ immutable งั้นเราลองมาดูตัวอย่าง use case ที่เราควรกำหนดตัวแปรเป็น immutable กันดีกว่า..

1.สร้างตัวแปรค่าคงที่ กันเผลอไปเปลี่ยนโดนไม่ตั้งใจ

class ParkingFee {
    int ratePerHour = 10
    ...
}
Enter fullscreen mode Exit fullscreen mode

สมมุติเราสร้างตัวแปรซึ่งเป็นค่าคงที่ ไม่สามารถเปลี่ยนแปลงได้ขึ้นมาตัวหนึ่ง

เราอาจจะบอกว่าไม่เห็นต้องประกาศระบุ immutable ขนาดนั้นเลยก็ได้นี่นา เราสร้างตัวแปรขึ้นมา เราก็จำได้อยู่แล้วว่าตัวแปรนี้มันห้ามเปลี่ยนค่า ก็อย่าซนไปเปลี่ยนค่ามันสิ ก็เท่านั้นเอง

ใช่แล้วครับ ถ้าคุณจำได้น่ะ ก็ไม่มีปัญหาหรอก แล้วถ้าคุณลืมล่ะ เช่นไม่ได้แตะโปรเจคนี้มาหลายๆ เดือนแล้ว หรือคุณอาจจะทำงานเป็นทีมกับโปรแกรมเมอร์คนอื่น เขาจะรู้มั้ยว่าตัวแปรนี้ห้ามเปลี่ยนค่า?

เดี๋ยวนี้โปรแกรมเมอร์ใช้งาน IDE กันเยอะ การมีโค้ดเด้งๆ ขึ้นมาเป็นเรื่องปกติ บางคนเพื่อนๆ คุณอาจจะไม่รู้ แต่เมื่อมีโค้ดเด้งขึ้นมาให้ ฉันก็นึกว่ามันใช้งานได้น่ะสิ!

class ParkingFee {
    final int ratePerHour = 10
    ...
}
Enter fullscreen mode Exit fullscreen mode

ดังนั้น ทางที่ดีเราควรกำหนดความเป็น immutable ให้ตัวแปรที่ไม่ต้องการให้เปลี่ยนค่าลงไปตรงๆ เลยจะดีกว่า

2.ลดการเกิดบั๊กจากการเปลี่ยนค่าตัวแปรโดยไม่ตั้งใจ

ลองดูโค้ดข้างล่างนี้หน่อย แล้วลองเดาดูว่า s.score ที่บรรทัดสุดท้ายจะมีค่าเป็นเท่าไหร่?

Student s = Student()
s.name = "Ann"
s.score = 50

displayStudentScore(s)

s.score = ?
Enter fullscreen mode Exit fullscreen mode

หลายคนน่าจะเดาว่ามันจะได้ 50 เนื่องจากเรากำหนดค่า s.score = 50 ตั้งแต่ข้างบน ถึงแม้มันจะผ่านฟังก์ชันที่เอาค่าเราไปปริ้น แต่ค่าก็ควรเป็น 50 เท่าเดิมสิ?

ใช่ครับ ค่ามันควรจะเป็น 50 เท่าเดิม ถ้าฟังก์ชันปริ้นมันทำอะไรข้างในประมาณนี้

function displayStudentScore(student){
    //Read-Only
    print(`${student.name} get score ${student.score}.`)
}
Enter fullscreen mode Exit fullscreen mode

แต่ถ้ามันมี requirement เข้ามาเพิ่มว่าเราต้องการ "เพิ่มคะแนนให้นักเรียนทุกคน คนละ 10 คะแนน" ล่ะ ... พอดีเหลือเกิ๊น! ที่วันนั้นคุณไม่ว่าง เลยเป็นเพื่อนคุณที่เข้ามาแก้โค้ดนี้ให้แทน ซึ่งเขาก็หาไปหามา แล้วก็เจอว่า ถ้าอยากให้คะแนนเพิ่ม งั้นก็บวกมันเพิ่มไปตรงนี้ละกัน

แบบนี้...

function displayStudentScore(student){

    //Mutable!: bonus score 10 point
    student.score += 10

    print(`${student.name} get score ${student.score}.`)
}
Enter fullscreen mode Exit fullscreen mode

เนื่องจาก object ทุกตัวเป็น reference type ดังนั้นการแก้ค่าของอ็อบเจคในฟังก์ชันก็จะส่งผลออกมาถึงด้านนอกด้วย นั่นแหละ ทำให้ค่า s.score ของเราอาจจะไม่ใช่ 50 แล้วก็ได้เมื่อมันโดนส่งค่าผ่านฟังก์ชันเข้าไป

แต่ปัญหานี้จะหมดไป ถ้าเรากำหนดค่าทั้งหมดเป็น immutable เพราะโค้ดข้างในฟังก์ชันก็จะแก้อะไรค่าของเราไม่ได้อีกแล้ว เขียนโค้ดด้านนอกได้อย่างสบายใจ

3.Shared Memory

หัวข้อนี้ไม่ได้เป็นประโยชน์ทำให้เราเขียนโค้ดดีขึ้น แต่ทำให้เราประหยัดการใช้เมโมรี่มากขึ้น

นั่นคือถ้าเรามีตัวแปร immutable อยู่ แล้วเราสร้างตัวแปรใหม่ขึ้นมาอีกตัวโดยให้มันถือตัวแปร immutable ของเราไว้ด้วย บางภาษาที่ตัว compiler ฉลาดๆ หน่อยจะมองออกว่าไม่ต้อง "copy" ตัวแปรออกมาใหม่อีกตัว สามารถใช้งานแชร์กันได้เลย เพราะถึงยังไงตัวแปรพวกนี้ก็เปลี่ยนค่าไม่ได้อยู่แล้ว

หรืออีกตัวอย่างหนึ่ง เช่นคลาส String ในภาษา Java (String ใน Java เป็นคลาสแบบ immutable นะ)

ถ้าเรากำหนดค่าตัวแปรขึ้นมา 2 ตัวในหัวเราจะคิดว่ามันต้องสร้าง instance ขึ้นมา 2 ชุด แต่ด้วยความเป็น immutable นั้นทำให้คอมไพเลอร์จะมีการเช็กว่าถ้า instance 2 ตัวนั้นมีค่าเหมือนกัน มันจะจัดตัวแปร 2 ตัวนั้นแชร์ค่ากันทันที

ทำการทดลองได้ด้วยลองสร้าง String 2ตัวที่มีค่าเหมือนกัน แล้วลองเช็กว่ามันเป็นตัวเดียวกันมั้ยด้วย == ที่ปกติแล้วจะได้ค่า false เสมอ (ปกติ Java ต้องเทียบสตริงด้วย equals() เท่านั้น)

ในตัวอย่างต่อไปขออธิบายด้วยภาษา Java นะ .. เพราะมันเป็นภาษาที่เคร่งแนวคิด OOP ประมาณนึงเลย

OOP และการใช้ getter/setter

ตามปกติแล้ว สำหรับคนที่เรียน OOP มาจะมีคอนเซ็ปหนึ่งที่เราต้องทำกันตลอดเวลา นั่นคือ getter และ setter

  • เพราะตามหลัก OOP เราจะต้องป้องกันไม่ให้คนอื่นเข้ามายุ่งกับตัวแปรภายในของเรา (กันคนแอบเปลี่ยนค่า) = เราเลยมักเซ็ต properties ทั้งหมดให้เป็น private
  • แล้วก็มีปัญหาตามมา นั่นคือถ้าเราล็อกไม่ให้คนอื่นเข้ามาใช้ค่าภายใน แล้วมันจะติดต่อกับอ็อบเจคตัวอื่นได้ยังไงล่ะ
  • วิธีแก้คือสร้างเมธอดขึ้นมา เป็นตัวกลางในการเข้าถึงตัวแปรระหว่างคลาสเรากับคลาสอื่นๆ
class Student {
    private String name;
    private int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setScore(int score) {
        this.score = score;
    }

    public int getScore() {
        return score;
    }
}
Enter fullscreen mode Exit fullscreen mode

เช่นตัวอย่างโค้ดข้างบน เรามี properties 2 ตัวคือ name และ score เราก็ต้องสร้างเมธอดที่เรียกว่า getter (เอาไว้ดึงค่าออกมา) และ setter (เอาไว้เซ็ตค่ากลับเข้าไป)

คนเรียน OOP ทุกคนจะต้องเขียนฟังก์ชันอันน่าเบื่อพวกนี้เยอะมาก เขียนจนชิน และโดยส่วนใหญ่จะเลิกเขียนเองแล้วใช้ IDE สร้างให้ก็ตาม (ฮา)

แต่จริงๆ แล้วนี่เป็นวิธีที่ไม่ดีเท่าไหร่! (อ้าว! แล้วฉันเรียนอะไรมาเนี่ย)

ก็เพราะการทำแบบนี้ให้ตัวอ็อบเจคของเรามีโอกาสเปลี่ยน state ค่าตัวแปรได้หลายจุดมากๆ หมายถึงถ้าเราส่งอ็อบเจคเราข้ามฟังก์ชันไปข้ามฟังก์ชันมา มีโอกาสที่อยู่ๆ ค่าของอ็อบเจคเราจะถูกเปลี่ยนโดยใครก็ไม่รู้ จับคนร้ายไม่ได้ด้วย (จับได้แหละ แต่ยาก!)

ดังนั้น FP จึงแนะนำให้คุณสร้างอ็อบเจคแบบ immutable ซะ

class Student {
    public final String name;
    public final int score;

    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
}
Enter fullscreen mode Exit fullscreen mode

เราประกาศ final ที่ properties ทุกตัวเลย ทีนี้คนอื่นก็จะแก้ค่าพวกนี้ไม่ได้แล้ว

สังเกตต่ออีกอย่างคือเราเอา getter/setter ออกไปด้วย แล้วเปิดให้ properties เป็น public แทน ... ก็ไม่เห็นต้องกลัวใครเปลี่ยนค่าแล้วไง มันแก้ค่าไม่ได้อยู่แล้ว

หลังๆ มานี่เวลาเราสร้าง class เราชอบสร้างในรูปแบบนี้เสมอเลยนะ เลิกเขียนกันทีกับ getter/setter โค้ดดูสะอาดขึ้น แถมเขียนสั้นลงและลดบั๊กอีกด้วย

แต่ก็ไม่จำเป็นนะว่าจะต้องรับค่าเข้ามาตรงๆ เราอาจจะรับค่าเข้ามาแล้วโปรเซสอะไรบางอย่างก่อนก็ยังไง

class Employee {
    public final String email;
    public final String name;

    public Employee(String firstname, String lastname) {
        this.name = firstname + " " + lastname;
        this.email = firstname + "." + lastname.substring(0,2) + "@tamemo.com"
    }
}
Enter fullscreen mode Exit fullscreen mode

Builder Pattern

ปัญหาจริงๆ ของการสร้างอ็อบเจคให้เป็น immutable คือเวลาเราจะแก้ค่าอะไร เราจะต้องสร้างอ็อบเจคใหม่ขึ้นมาเลย

เช่นต้องการ +10 คะแนนให้ Student หนึ่งคน

Student stu1 = new Student("Ann", 50);
Student stu2 = new Student(stu1.name, stu1.score + 10);
Enter fullscreen mode Exit fullscreen mode

ก็ต้องสร้างอ็อบเจคตัวใหม่ แล้ว copy ค่าจากอ็อบเจคตัวเก่ามาใส่ตัวใหม่ให้หมดเลย

เอาจริงมันก็ไม่ใช่ปัญหาหรอก แค่มันเขียนยาววววขึ้น 55

ดังนั้นเราอาจจะเอา "Builder Pattern" มาช่วย ก็จะทำให้การ copy ค่าทำได้ง่ายขึ้น

Builder ซึ่งเป็นหนึ่งใน Design Pattern ซึ่งยังไม่ขอพูดถึงในบทนี้ ในอนาคตจะมาเขียนเกี๋ยวกับเรื่องนี้ต่ออีกทีหนึ่งนะ

class Data {
    int x, y, z, p, q, r ;
}

Data data1 = new Data(1, 2, 3, 4, 5, 6);

// Standard
Data data2 = new Data(
    10, data1.y + 100, data1.z * 2, data1.p, data1.q, data1.r
);

// Builder Pattern
Data data2 = new Data.Builder()
    .from(data1)
    .setX(10)
    .setY(data1.y + 100)
    .setZ(data1.z * 2)
    .build();
}
Enter fullscreen mode Exit fullscreen mode

สรุป

การทำให้ตัวแปรเปลี่ยนค่าไม่ได้ อาจจะทำให้คนที่เพิ่งเรียนเขียนโปรแกรมแปลกใจกับแนวคิดนี้ (เพราะที่มหาลัยส่วนใหญ่จะไม่ได้สอนข้อเสียของ OOP) แต่เชื่อเถอะว่าการที่ตัวแปรเปลี่ยน state ได้น่ะ ทำให้เกิดบั๊กมากมายตามมา

ไม่อย่างนั้นภาษายุคใหม่ที่ออกมาแต่ละตัว คงไม่สร้างชนิดตัวแปรแบบ immutable ออกมาให้เราใช้กันหรอก บางภาษาถ้าตัวแปรนั้นเป็น mutable แล้วคอมไพเลอร์เช็กเจอว่าเราไม่ได้เปลี่ยนค่าอะไร บางทีเจอ warning ด้วยซ้ำ (มันจะบอกว่าเปลี่ยนเป็น immutable เถอะ)

ถ้าเป็นไปได้พยายามใช้ตัวแปรแบบ immutable ทั้งหมดนะครับ

Top comments (0)