Ruby 2.4 gộp Fixnum và Bignum vào cùng một lớp (Integer), vì vậy tôi nghĩ đây là lúc tốt để xem lại các kiểu số khác nhau trong Ruby!
Đó là những gì chúng ta sẽ nói trong bài viết này 🙂
Tổng quan các kiểu số
Hãy bắt đầu bằng cách nhìn vào hệ thống phân cấp lớp của tất cả các lớp liên quan đến số trong Ruby:
Numeric
Integer
Fixnum
Bignum
Float
Complex
Rational
BigDecimal (Thư viện chuẩn)
Như bạn thấy, lớp Numeric là cha của tất cả các lớp số. Nhớ rằng bạn có thể dùng phương thức ancestors để khám phá các lớp cha cho bất kỳ lớp nào.
Ví dụ:
Fixnum.ancestors - Fixnum.included_modules [Fixnum, Integer, Numeric, Object, BasicObject]
Giờ hãy xem các lớp này dưới dạng bảng:
| Lớp | Mô tả | Ví dụ |
|---|---|---|
| Integer | Lớp cha của Fixnum & Bignum |
1 |
| Fixnum | Số nguyên phù hợp kiểu integer của HĐH (32 hoặc 64 bit) | 1 |
| Bignum | Dùng cho số lớn hơn | 111111111111 |
| Float | Số thập phân không chính xác | 5.0 |
| Complex | Dùng cho toán học với số phức | (1+0i) |
| Rational | Dùng để biểu diễn phân số | (2/3) |
| BigDecimal | Số thập phân độ chính xác hoàn hảo | 3.0 |
Sự không chính xác của Float
Lớp Float trong Ruby được mô tả là "không chính xác" trong tài liệu Ruby chính thức.
Tại sao lại như vậy?
Hãy cho tôi ví dụ:
0.2 + 0.1 == 0.3 # false
Tại sao điều này là false?
Hãy nhìn kết quả của 0.2 + 0.1:
0.30000000000000004
Chính xác! Đó là ý của sự không chính xác.
Điều này xảy ra vì cách một float được lưu trữ. Nếu bạn cần số thập phân luôn chính xác, bạn có thể dùng lớp BigDecimal.
Float vs BigDecimal
BigDecimal là lớp cho số thập phân tùy ý độ chính xác (arbitrary-precision).
Ví dụ:
require 'bigdecimal'
BigDecimal("0.2") + BigDecimal("0.1") == 0.3 # true
Tại sao chúng ta không luôn dùng BigDecimal? Vì nó chậm hơn nhiều!
Đây là benchmark:
Calculating ------------------------------------- bigdecimal 21.559k i/100ms float 79.336k i/100ms ------------------------------------------------- bigdecimal 311.721k (± 7.4%) i/s - 1.552M float 3.817M (±11.7%) i/s - 18.803M Comparison: float: 3817207.2 i/s bigdecimal: 311721.2 i/s - 12.25x slower
BigDecimal chậm gấp 12 lần Float, và đó là lý do nó không phải mặc định 🙂
Fixnum vs Bignum
Trong phần này, tôi muốn khám phá sự khác biệt giữa Fixnum và Bignum trước Ruby 2.4.
Hãy bắt đầu với mã:
1.class # Fixnum 100000000000.class # Bignum
Ruby tạo lớp đúng cho chúng ta, và sẽ tự động thăng cấp Fixnum thành Bignum khi cần thiết.
Lưu ý: Bạn có thể cần số lớn hơn để có đối tượng
Bignumnếu bạn dùng trình thông dịch Ruby 64-bit.
Tại sao chúng ta cần các lớp khác nhau? Câu trả lời là để làm việc với số lớn hơn, bạn cần triển khai khác nhau, và làm việc với số lớn chậm hơn, nên chúng ta kết thúc với tình huống tương tự Float vs BigDecimal.
Đặc điểm đặc biệt của Fixnum
Lớp Fixnum cũng có một số đặc điểm đặc biệt. Ví dụ, object_id được tính bằng công thức.
1.object_id # 3 20.object_id # 41
Công thức là: (number * 2) + 1.
Nhưng có thêm, khi bạn dùng Fixnum, không có đối tượng nào được tạo ra cả. Không có dữ liệu để lưu trữ trong Fixnum, vì giá trị được suy ra từ chính object_id.
Đây chỉ là chi tiết triển khai, nhưng tôi nghĩ nó thú vị để biết 🙂
MRI (Matz's Ruby Interpreter) dùng hai macro này để chuyển đổi giữa giá trị và object_id:
INT2FIX(i) ((VALUE)(((SIGNED_VALUE)(i))<<1 | FIXNUM_FLAG)) FIX2LONG(x) ((long)RSHIFT((SIGNED_VALUE)(x),1))
Điều xảy ra ở đây gọi là "bit shifting" (dịch bit), di chuyển tất cả bit sang trái hoặc phải.
Dịch một vị trí sang trái tương đương nhân 2 và đó là lý do công thức là (number * 2) + 1. +1 đến từ FIXNUM_FLAG.
Ngược lại, Bignum hoạt động hơn như một lớp bình thường và dùng object_id bình thường:
111111111111111.object_id # 23885808
Tất cả điều này nghĩa là đối tượng Fixnum gần với symbol về mặt cách hoạt động ở cấp trình thông dịch, trong khi đối tượng Bignum gần với string hơn.
Integer trong 2.4
Kể từ Ruby 2.4, Fixnum và Bignum đã deprecated, nhưng bên dưới chúng vẫn hoạt động y nguyên cách cũ.
Ruby tự động chuyển từ kiểu này sang kiểu khác.
Không thay đổi lớp.
Điều này nghĩa là số Integer nhỏ vẫn hoạt động giống như Fixnum.
Tóm tắt
Trong bài viết này, bạn đã học về các lớp liên quan đến số tồn tại trong Ruby.
Bạn học được float không chính xác và bạn có thể dùng BigDecimal nếu độ chính xác quan trọng hơn hiệu năng nhiều. Bạn cũng học được đối tượng Fixnum đặc biệt ở cấp trình thông dịch, nhưng Bignum chỉ là đối tượng bình thường.
Nếu bạn thấy bài viết này thú vị, đừng quên đăng ký newsletter của tôi trong form bên dưới 🙂