Monthly Archives: กรกฎาคม 2007

Hash table ( ตารางแฮช ) 2/2

ไปหน้าแรก | สารบัญ | Laploy.comระเบียนบทความ | บทความจากลาภลอย

สร้าง Hash table ( ตารางแฮช )

ด้วยภาษา C++ และจาวาตอนจบ

ท่านผู้อ่านท่านหนึ่งตั้งกระทู้ถามมาในเว็บบอร์ด laploy.com ว่าดังนี้ “อยากทราบว่า Hashtable ทำงานอย่างไร ใช้กับอะไรบ้าง ในกรณีไหนที่ควรใช้หรือไม่ควร อยากให้อาจารย์ช่วยอธิบายให้หน่อยครับ ขอพระคุณครับ” ผมจึงตัดสินใจเขียนรับใช้เป็นบทความที่คุณจะได้อ่านต่อไปนี้ เนื่องจาก Blog ของ Space โพสหนึ่งตอนยาวมากไม่ได้ ผู้เขียนจึงแบ่งออกเป็นสองโพส ที่ท่านกำลังอ่านอยู่นี้คือครึ่งหลังซึ่งเป็นตอนจบ

 

ฟังก์ชัน initIterator()

ฟังก์ชัน initIterator() ทำหน้าที่กำหนดค่าเริ่มต้นให้แก่ตัวแปรบางตัวของคลาสซึ่งทำหน้าที่จัดการลิงค์ลิสต์ ฟังก์ชัน initIterator() ไม่ต้องการอาร์กิวเมนต์ใดๆ และไม่คืนค่าใดๆ ดังแสดงในตัวอย่างโปรแกรมถัดไป ฟังก์ชัน initIterator() จะถูกเรียกโดยฟังก์ชัน displayAll() ซึ่งเป็นฟังก์ชันที่ทำหน้าที่แสดงข้อมูลทั้งหมดภายในตารางแฮช คุณจะได้เรียนเรื่องฟังก์ชัน displayAll() ในหัวข้อ “การสร้างตารางแฮชด้วยภาษา C++” ต่อไปในบทนี้

เมื่อฟังก์ชัน initIterator() ถูกเรียกมันจะกำหนดค่าให้สมาชิกข้อมูลสองตัวของคลาส Hashtable สมาชิก current_entry จะได้รับการกำหนดค่าให้เป็น NULL และสมาชิก current_index จะถูกกำหนดให้มีค่าเท่ากับสมาชิก tablesize

ต่อไปลูป for จะหาหน่วยข้อมูลแรกในตารางแฮช ในแต่ละครั้งของการวนลูป หน่วยข้อมูลตัวปัจจุบันของอาร์เรย์ table จะถูกนำไปเปรียบเทียบกับค่า NULL หากหน่วยข้อมูลมีค่าเป็น NULL การค้นจะดำเนินต่อไปเพื่อหาหน่วยข้อมูลตัวที่ไม่เป็น NULL หน่วยข้อมูลตัวแรกที่มิใช่ NULL จะกลายเป็นหน่วยข้อมูลแรกในตารางแฮช

เมื่อพบหน่วยข้อมูลแรกแล้ว หน่วยที่คู่กันในอาร์เรย์ table จะถูกนำไปใส่ใน current_entry ค่านี้เป็นค่าอ้างอิงไปยังโครงสร้าง metadata ที่เก็บค่า key และ value ของหน่วยข้อมูลนั้น ค่าดรรชนีของหน่วยนั้นจะถูกนำไปเก็บไว้ใน สมาชิก current_index แล้วฟังก์ชัน initIterator() ก็จะจบการทำงานเพียงเท่านี้

ฟังก์ชัน displayAll() จะเรียกใช้งานฟังก์ชัน initIterator() เพื่อตรวจหาหน่วยข้อมูลตัวแรกในตารางแฮชก่อนการเรียกใช้งานฟังก์ชัน hasNext() และ getNextKey() เพราะทั้งสองฟังก์ชันนี้จะเรียกใช้สมาชิก current_entry และ current_index โดยตรง เราจะดูรายละเอียดของสองฟังก์ชันนี้ในหัวข้อถัดไป

void initIterator(){
    current_entry = NULL;
    current_index = tablesize;
    for(int I=0; I<tablesize; I++){
        if(table[I] == NULL){
            continue;
        }else{
            current_entry = table[I];
            current_index = i;
            break;
        }
    }
}

 

ฟังก์ชัน hasNext() และ getNextKey()

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

ฟังก์ชัน hasNext() ทำการตรวจสอบโดยเปรียบเทียบค่าของ current_entry กับ NULL หากค่าของ current_entry เป็น NULL ฟังก์ชัน hasNext() จะคืนค่าบูลีนเป็นเท็จ มิฉะนั้นจะคืนค่าบูลีนเป็นจริง

bool hasNext(){
    if(current_entry == NULL){
        return false;
    }else{
        return true;
    }
}

 

ดังที่คุณคงยังจำได้จากหัวข้อ “ฟังก์ชัน initIterator()” ว่าทั้ง current_entry และ current_index ล้วนเป็นสมาชิกข้อมูลของคลาส Hashtable โดย current_entry ทำหน้าที่เก็บค่าอ้างอิงไปยังหน่วยข้อมูลปัจจุบันในตารางแฮช ส่วน current_index เก็บค่าดรรชนีของอาร์เรย์ table ที่ชี้ไปยังหน่วยข้อมูลปัจจุบัน

ฟังก์ชัน getNextKey() ทำหน้าที่อ่านค่า key จากหน่วยข้อมูลที่ current_entry ชี้อยู่ ฟังก์ชัน getNextKey() จะเคลื่อนไปยังหน่วยข้อมูลถัดไปในตารางแฮชโดยจะพยายามไปยังส่วนถัดไปของลิงค์ลิสต์ก่อน หากส่วนถัดไปของลิงค์ลิสต์มีค่าเป็น NULL มันจะเคลื่อนไปยังดรรชนีต่อไปของอาร์เรย์ และท่องอาร์เรย์ไปจนกว่าจะพบหน่วยข้อมูลตัวถัดไป

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

ก่อนเริ่มกระบวนการฟังก์ชัน getNextKey() จะตรวจสอบก่อนว่าค่าของ current_entry เท่ากับ NULL หรือไม่ หากพบว่าเป็นเช่นนั้น มันจะนำค่า NULL ไปใส่ที่ตำแหน่งแรกในอาร์เรย์ key (การทำเช่นนี้เท่ากับเซตให้ key ว่างเปล่า) แล้วฟังก์ชัน getNextKey() จะออกจากการทำงานโดยไม่ดำเนินการใดๆ ต่อไปอีก

หากค่าของ current_entry ไม่เท่ากับ NULL มันจะคัดลอกค่า key ของโครงสร้าง metadata ที่เก็บหน่วยข้อมูลปัจจุบันไปใส่ในพอยน์เตอร์ key โดยการเรียกใช้ฟังก์ชัน strcpy() เมื่อคัดลอกเสร็จแล้วฟังก์ชัน getNextKey() จะค้นหาหน่วยข้อมูลถัดไป ซึ่งทำได้โดยอาศัยค่าใน next ซึ่งเป็นสมาชิกข้อมูลของโครงสร้าง metadata อันที่เป็นหน่วยข้อมูลปัจจุบัน

ค่าของ next จะถูกนำมาเปรียบเทียบกับ NULL ในคำสั่ง if หาค่าของ next ไม่เป็น NULL แสดงว่ายังมีหน่วยข้อมูลอื่นอยู่ภายในลิงค์ลิสต์ที่ตำแหน่งดรรชนีนี้อีก ค่าของ next จะถูกนำไปใส่ใน current_entry ทำให้หน่วยข้อมูลถัดไปกลายเป็นหน่วยข้อมูลปัจจุบันของตารางแฮช

อย่างไรก็ดีหากค่า next เท่ากับ NULL ฟังก์ชัน getNextKey() จะข้ามแต่ละหน่วยในอาร์เรย์ table ไปเพื่อหาหน่วยในอาร์เรย์ที่ไม่เป็น NULL ซึ่งหากหาเจอฟังก์ชัน getNextKey() จะคัดลอกค่าของ AE ไปใส่ไว้ใน current_entry และคัดลอกค่าดรรชนีของ AE นั้นไปใส่ใน current_index แล้วฟังก์ชัน getNextKey() ก็จะจบการทำงาน

หากหน่วยที่ยังเหลือในอาร์เรย์ table เป็น NULL ทั้งหมดแสดงว่าไม่มีหน่วยข้อมูลในตารางแฮชแล้ว ฟังก์ชัน getNextKey() ก็จะนำค่า NULL ไปใส่ใน current_entry และนำค่าใน tablesize ไปใส่ใน current_index ค่า key และ value นี้ถูกดึงออกมาจากตารางแฮชโดยไม่มีลำดับที่เฉพาะเจาะจงแต่อย่างใด เพราะโดยทั่วไปแล้วตารางแฮชไม่สนับสนุนการเก็บข้อมูลโดยเรียงตามลำดับ

void getNextKey(char* key){
    if(current_entry == NULL){
        key[0] = 9192;
        return;
    }
    strcpy(key, current_entry->key);
    if(current_entry->next != NULL){
        current_entry = current_entry->next;
    }else{
        for(int i=current_index+1; i<tablesize; i++){
            if(table[i] == NULL){
                continue;
            }
            current_entry = table[i];
            current_index = i;
            return;
        }
        current_entry = NULL;
        current_index = tablesize;
    }
}

 

สร้างตารางแฮชด้วยภาษา C++

ตอนนี้คุณรู้แล้วว่าส่วนประกอบต่างๆ ของคลาส Hashtable ทำงานอย่างไร ได้เวลานำชิ้นส่วนต่างๆ มาประกอบเข้าด้วยกันเป็นโปรแกรมประยุกต์ใช้งานด้วยภาษา C++ เสียที เราจะแบ่งโปรแกรมออกเป็นสามไฟล์ ประกอบด้วย HashtableDemo.cpp เป็นไฟล์เก็บตัวโปรแกรมประยุกต์ใช้งาน HashTable.h เป็นไฟล์เฮดเดอร์ทำหน้าที่เก็บนิยามของโครงสร้าง metadata และคลาส Hashtable และไฟล์ Hashtable.cpp ทำหน้าที่เก็บโค้ดของฟังก์ชันต่างๆ ที่ท้ายหัวข้อนี้จะมีโค้ดของโปรแกรมทั้งสามไฟล์โดยสมบูรณ์ ในหัวข้อนี้เราจะมาให้ความสนใจกับวิธีนำคลาส Hashtable มาประยุกต์ใช้ ส่วนเรื่องเมธอดและสมาชิกต่างๆ ของคลาสนั้นคุณได้เรียนไปเรียบร้อยแล้ว

โปรแกรมจะเริ่มการทำงานโดยประกาศอินสแตนซ์ของคลาส Hashtable แล้วกำหนดค่าอ้างอิงของคลาสให้กับ พอยน์เตอร์ชื่อ hashtable จากนั้นเราประกาศตัวแปรแบบ char สองตัวให้เป็นค่าจากมาโคร SIZE_KEY และ SIZE_VALUE ที่เรานิยามไว้เพื่อกำหนดขนาดของอาร์เรย์ สองอาร์เรย์นี้ทำหน้าที่เก็บ key และ value ของข้อมูลที่คุณจะใส่เข้าไปในตารางแฮช

ต่อมาเราใช้ฟังก์ชัน strcpy() คัดลอกค่า key และ value ไปใส่ในอาร์เรย์ key และ value แต่ก่อนที่เราจะนำมันแทรกเข้าไปในตารางได้ เราจะต้องตรวจสอบก่อนว่ามี key ค่านี้อยู่ในตารางแฮชแล้วหรือยังด้วยการเรียกใช้ฟังก์ชัน contains() อันเป็นฟังก์ชันสมาชิกของคลาส Hashtable() ฟังก์ชัน contains() จะคืนค่าบูลีนเป็นจริงหากมี key นั้นแล้ว ถ้าไม่เช่นนั้นก็จะคืนค่าบูลีนเป็นเท็จ ให้สังเกตว่าเรานำตัวกระทำ not (!) มาใช้เพื่อกลับตรรกะของค่าที่ฟังก์ชันส่งมา เราทำเช่นนี้เพื่อให้บรรทัดคำสั่งภายใต้ if ทำงานต่อไป

หากยังไม่มี key ค่านั้นในตารางแฮช โปรแกรมจะแสดงข้อความว่าค่า key และ value ได้ถูกแทรกเข้าไปในตารางแฮชแล้ว การแทรกจริงๆ เกิดขึ้นตอนเรียกฟังก์ชัน put() ซึ่งเป็นฟังก์ชันสมาชิกของคลาส Hashtable ฟังก์ชัน put() ต้องการอาร์กิวเมนต์สองตัวคือค่า key และ value

เราทำเช่นนี้อีกสองหนเพื่อแทรกค่า key และ value อีกสองชุดเข้าไปในตารางแฮช ข้อมูลเหล่านี้จะถูกแสดงออกมาบนจอภาพดังนี้ และหน้าตาของตารางแฮชจะเป็นดังรูปที่ 5

 

รูปที 11-5 สภาพของตารางแฮชหลังการทำงานของฟังก์ชัน put() ครั้งสุดท้าย

 

Adding node – key: 389 value: Mary

Adding node – key: 415 value: Henry

Adding node – key: 999 value: Joe

เมื่อค่าทั้งสามถูกแทรกเข้าไปเรียบร้อยแล้ว โปรแกรมจะเรียกฟังก์ชัน displayAll() ซึ่งเป็นฟังก์ชันที่อยู่เดี่ยวๆ ไม่ใช่ฟังก์ชันสมาชิกของคลาส Hashtable เพื่อแสดงข้อมูลภายในตารางแฮช ฟังก์ชันนี้ถูกนิยามไว้ใต้ฟังก์ชัน main() ในตัวอย่างนี้

เมื่อฟังก์ชันทำงานเราจะเห็นข้อความต่อไปนี้ปรากฏบนจอภาพ

Current node is hashtable:

Key: 415 value: Henry

Key: 999 value: Joe

Key: 389 value: Mary

จากนั้นโปรแกรมจะเรียกฟังก์ชัน remove() ซึ่งเป็นฟังก์ชันสมาชิกของคลาส Hashtable เพื่อลบหน่วยข้อมูลในตารางที่มีค่า key เป็น 415 เมื่อลบเสร็จแล้วก็จะเรียกฟังก์ชัน displayAll() อีกครั้งเพื่อแสดงว่าหน่วยข้อมูลได้ถูกลบออกไปจากตารางแฮชแล้วจริงๆ

After removing 415:

Current nodes in hashtable:

Key: 999 value: Joe

Key: 389 value: Mary

ลำดับสุดท้ายโปรแกรมจะทำลายตารางแฮชโดยใช้ตัวกระทำ delete และ destructor จะถูกเรียกให้ทำงานโดยอัตโนมัติก่อนที่ตารางแฮชจะถูกทำลาย destructor จะเรียกฟังก์ชัน removeAll() อันเป็นฟังก์ชันสมาชิกของคลาส Hashtable ซึ่งจะแสดงหน่วยข้อมูลแต่ละตัวบนจอก่อนที่จะลบออกจากตารางแฮช สิ่งที่ปรากฏบนจอภาพหลังจากหน่วยข้อมูลทั้งหมดถูกลบออกไปแล้วเป็นดังนี้

Destroying hashtable:

Remove node – key: 999 Joe

Remove node – key: 389 Mary

ซอร์โค้ดสมบูรณ์ในภาษา C++

#include <iostream.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include 93Hashtable.h
void displayAll(Hashtable* hashtable);
void main(){
    Hashtable* hashtable = new Hashtable();
    char key[SIZE_KEY];
    char value[SIZE_VALUE];
    strcpy(key, 9338994);
    strcpy(value, 93Mary94);
    if(!hashtable->contains(key)){
        cout << 93Adding node 96 key: 93 << key << 93value: 93 << value << endl;
        hashtable->put(key, value)
    }
    strcpy(key, 9341594);
    strcpy(value, 93Henry94);
    if(!hashtable->contains(key)){
        cout << 93Adding node 96 key: 93 << key << 93value: 93 << value << endl;
        hashtable->put(key, value)
    }
    strcpy(key, 9399994);
    strcpy(value, 93Joe94);
    if(!hashtable->contains(key)){
        cout << 93Adding node 96 key: 93 << key << 93value: 93 << value << endl;
        hashtable->put(key, value)
    }
    displayAll(hashtable);
    hashtable->remove(9341594);
    cout << 93After removing 415:94 << endl;
    displayAll(hashtable);
    count << 93\nDestroying hashtable:94 << endl;
    delete hashtable;
}

void displayAll(Hashtable* hashtable){
    char key[SIZE_KEY];
    char value[SIZE_VALUE];
    cout << 93\nCurrent nodes in hashtable:94 << endl;
    hashtable->initIterator();
    while(hashtable->hasNext()){
        hashtable->getNextKey(key);
        hashtable->get(key, value);
        cout << 93key: 93 << key << 93\tvalue: 93 << value << endl;
    }
}

/////////////// HashTable.h //////////////////
#include <string.h>
#define SIZE_KEY32
#define SIZE_VALUE256
#define DEFAULT_TABLESIZE101

typedef struct Metadata{
    struct Metadata(char* key, char* value){
        strcpy(this->key, key);
        strcpy(this->value, value);
        next = NULL;
    }
    char key[SIZE_KEY];
    char value[SIZ_VALE];
    struct Metadata* next;
}METADAT;

class Hashtable{
    private:
    int tablesize;
    METADATA** table;
    int size;
    METADATA* current_entry;
    Int current_index;
    Long hashString(char* key);
    METADATA* find(char* key);
public:
    Hashtable(int tablesize = DEFAULT_TABLESIZE);
    virtual ~Hashtable();
    bool put(char* key, char* value);
    bool get(char* key, char* value);
bool contains(char* key);
bool remove(char* key);
void removeAll();
int getSize();
void initIterator();
bool hashNext();
void getNextKey(char* key);
};

////////////////// Hashtable.cpp ////////////////
#include <iostream.h>
#include <stdlib.h>
#include 93Hashtable.h
Hashtable::Hashtable(int tablesize){
    Size = 0;
    this->tablesize = tablesize;
    table = new METADATA* [tablesize];
    for(int I=0; I<tablesize; I++){
        table[I] = NULL;
    }
}

Hashtable::~Hashtable(){
    removeAll();
    delete[] table;
}

bool Hashtable::put(char* key, char* value){
    if(find(key) != NULL){
        return false;
    }
    METADATA* entry = new METADATA(key, value);
    int bucket = hashString(key);
    entry->next = table[bucket];
    table[bucket] = entry;
    size++;
    return true;
}

bool Hashtable::get(char* key, char* value){
    METADATA* temp = find(key);
    if(temp == NULL){
        value[0] = 9192;
        return false;
    }else{
        strcpy(value, temp->value);
        return true;
    }
}

bool Hashtable::contains(char* key){
    if(find(key) == NULL){
        return false;
    }else{
        return true;
    }
}

bool Hashtable::remove(char* key){
    int bucket = hashString(key);
    METADATA* temp = table[bucket];
    if(temp == NULL){
        return false;
    }else if(strcmp(key, temp->key) == 0) {
        table[bucket] = temp->next;
        delete temp;
        size--;
        return true;
    }else{
        METADATA* temp_next = temp->next;
        while(temp_next != NULL){
            if(strcmp(key, temp_next->key) == 0){
                temp->next = temp_next->next;
                delete temp_next;
                size--;
                return true;
            }
            temp = temp->next;
            temp_next = temp_next->next;
        }
    }
    return false;
}

void Hashtable::removeAll(){
    for(int i=0; i<tablesize; i++){
        METADATA* temp = table[i];
        while(temp != NULL){
            METADATA* next = temp->next
            cout << 93Removing node 96key:94 << temp->key << 93\t94 << temp->value << endl;
            delete temp;
            temp = next;
        }
    }
    size = 0;
}

int Hashtable::getSize(){
    return size;
}

METADATA* Hashtable::find(char* key){
    int bucket = hashString(key);
    METADATA* temp = table(bucket);
    while(temp != NULL){
        if(strcmp(key, temp->key) == 0){
            return temp;
        }
        temp = temp->next;
    }
    return NULL;
}

long Hashtable::hashString(char* key){
    int n = strlen(key);
    long h = 0;
    for(int i=0; i<n; i++){
        h = (h << 2) + key[i];
    }
    return abs(h % tablesize);
}

void Hashtable::initIterator(){
    current_entry = NULL;
    current_index = tablesize;
    for(int I=0; I<tablesize; I++){
        if(table[I] == NULL){
            continue;
        }else{
            current_entry = table[I];
            current_index = i;
            break;
        }
    }
}

bool Hashtable::hasNext(){
    if(current_entry == NULL){
        return false;
    }else{
        return true;
    }
}

void Hashtable::getNextKey(char* key){
    if(current_entry == NULL){
        key[0] = 9192;
        return;
    }
    strcpy(key, current_entry->key);
    if(current_entry->next != NULL){
        current_entry = current_entry->next;
    }else{
        for(int i=current_index+1; i<tablesize; i++){
            if(table[i] == NULL){
                continue;
            }
            current_entry = table[i];
            current_index = i;
            return;
        }
        current_entry = NULL;
        current_index = tablesize;
    }

 

สร้างตารางแฮชในภาษาจาวา

การสร้างโปรแกรมประยุกต์ใช้งานตารางแฮชในภาษาจาวาง่ายกว่าภาษา C++ เนื่องจากจาวาได้เตรียมคลาส Hashtable มาไว้ให้เรียบร้อยแล้ว โดยเป็นส่วนหนึ่งของ Java Collection Classes ซึ่งนิยามไว้ในเพ็คเกจ java.util โดยมีคลาสที่เจตนาทำมาให้ใช้งานเกี่ยวกับตารางแฮชสองคลาสคือคลาส Hashtable และคลาส HashMap สองคลาสนี้แตกต่างกันตรงการทำงานที่เกี่ยวข้องกับเธรด (thread)

คลาส Hashtable เป็นคลาสแบบ synchronize ทำให้สามารถใช้งานอินสแตนซ์ของมันได้อย่างปลอดภัยจากหลาย เธรดพร้อมๆ กัน ส่วน HashMap มิได้มีการทำงานแบบ synchronize จึงเหมาะที่จะใช้งานกับเพียงเธรดเดียว โปรเซสหลายๆ โปรเซสสามารถเรียกใช้งานคลาส Hashtable ได้พร้อมๆ กันโดยไม่มีปัญหา ส่วนคลาส HashMap เหมาะสำหรับใช้กับโปรเซสเพียงโปรเซสเดียวเท่านั้น

ต่อไปนี้เราจะมาดูวิธีสร้างโปรแกรมประยุกต์ในเวอร์ชันภาษาจาวาซึ่งมีการทำงานเทียบเท่าเวอร์ชันภาษา C++ ที่คุณได้เรียนวิธีสร้างมาแล้วในหัวข้อก่อนหน้านี้ ตัวโปรแกรมทั้งหมดจะแสดงอยู่ที่ส่วนท้ายของหัวข้อนี้ เราจะเริ่มโดยประกาศอินสแตนซ์ของคลาส Hashtable แล้วกำหนดชื่อของอินสแตนซ์ว่า hashtable

ต่อไปเราประกาศอินสแตนซ์ของคลาส String สองอินสแตนซ์โดยตั้งชื่อว่า key และ value แล้วนำข้อมูลที่เราต้องการนำไปแทรกในตารางแฮชเป็นหน่วยข้อมูลแรกไปใส่ในสองอินสแตนซ์นี้ แต่ก่อนจะแทรกข้อมูลเราต้องตรวจสอบก่อนว่าในตารางแฮชมี key ค่าเดียวกันนี้หรือยัง ซึ่งทำได้โดยเรียก containsKey() ซึ่งเป็นเมธอดสมาชิกของคลาส Hashtable การทำเช่นนี้เทียบได้กับการเรียกใช้ฟังก์ชัน contains() ในโปรแกรมเวอร์ชันภาษา C++

ต่อไปเราจะแสดงคู่ของค่า key/value บนจอภาพก่อนเรียก put() อันเป็นเมธอดสมาชิกของคลาส Hashtable เพื่อทำการแทรกข้อมูลคู่นี้เข้าไปในตารางแฮช เมธอด put() อันเป็นเมธอดสมาชิกของคลาส Hashtable นี้เทียบได้กับฟังก์ชัน put() ที่คุณได้เรียนวิธีสร้างในภาษา C++ ไปแล้ว

เราทำอย่างนี้ซ้ำสองหน ผลลัพธ์ที่ได้คือชุดข้อมูล key/value สามชุดถูกแทรกเข้าไปในตาราง เมื่อเสร็จแล้วเราจะเห็นข้อความบนจอภาพดังนี้

Adding node – key: 389 value: Mary

Adding node – key: 415 value: Henry

Adding node – key: 999 value: Joe

ขั้นตอนต่อไปโปรแกรมของเราจะดึงข้อมูลหน่วยที่มี key เท่ากับ 415 ออกมา ซึ่งทำได้ด้วยการเรียก get() อันเป็นเมธอดสมาชิกของคลาส Hashtable และส่งค่า key ให้มัน เมธอด get() จะคืนค่าเป็นมาเป็นออบเจ็กต์ที่มี key ตรงกับที่เราต้องการ หรือไม่ก็คืนค่ามาเป็น NULL ค่าที่ได้รับมาจะถูกแปลงเป็นออบเจ็กต์แบบ String หากค่าไม่เป็น NULL โปรแกรมจะแสดงค่าของมันออกมาบนจอภาพดังนี้

Retrieving a value out of the hashtable:

Found key: 999 value: Henry

จากนั้นโปรแกรมจะแสดงขนาดของตารางแฮชและหน่วยข้อมูลทั้งหมดภายในตาราง เราตรวจสอบขนาดของตารางได้โดยการเรียกใช้ size() อันเป็นเมธอดสมาชิกของคลาส Hashtable ขนาดจะถูกแสดงออกบนจอภาพดังนี้

Size of hashtable before removing nodes: 3

การแสดงหน่วยข้อมูลทำได้โดยเรียก displayEntries() ซึ่งเป็นเมธอดอิสระที่เรานิยามไว้ถัดจากเมธอด main() ของโปรแกรม

เมธอด displayEntries() ทำงานโดยสร้าง set ของค่า key ทั้งหมดในตารางแฮช อย่างที่คุณน่าจะจำได้จากตอนเรียนการเขียนโปรแกรมภาษาจาวาว่า set เป็นออบเจ็กต์ที่ใช้เก็บกลุ่มของค่าเพื่อให้ iterator จัดการ iterator คือคลาสที่มีเมธอดสำหรับโยกย้ายและจัดการในลักษณะต่างๆ กับค่าที่อยู่ภายใน set

เราใช้ keySet() อันเป็นเมธอดของคลาส Hashtable ในการสร้าง set ที่บรรจุไว้ด้วย key ที่นำมาจากตารางแฮช เมื่อสร้าง set แล้วเมธอด displayEntries() จะสร้างตัว iterator เมธอด hasNext() และ next() อันเป็นเมธอดสมาชิกของคลาส iterator มีไว้เพื่อให้เราสามารถท่องไปภายใน set ตามค่า key

ขั้นแรกเราเรียกเมธอด hasNext() โดยใช้เป็นเงื่อนไขของลูป while ซึ่งจะตรวจสอบว่ายังมีหน่วยข้อมูลถัดไปใน set หรือไม่ เมธอด hasNext() จะคืนค่าบูลีนเป็นจริงหากมีหน่วยข้อมูลถัดไป มิฉะนั้นก็จะคืนค่าบูลีนมาเป็นเท็จ

หากมีหน่วยข้อมูลถัดไปโปรแกรมจะเรียกเมธอด next() ซึ่งจะขยับไปยังหน่วยข้อมูลถัดไปแล้วส่งค่า key ของหน่วยข้อมูลนั้นมาให้ ค่า key นี้ถูกส่งกลับมาในรูปของออบเจ็กต์ เราจึงต้องแปลงให้เป็น string เสียก่อนแล้วจึงนำไปใส่ใน key string ค่านี้จะถูกส่งไปให้เมธอด get() เพื่อดึงข้อมูลที่ตรงกับ key นี้มา เมธอด get() จะคืนค่ามาเป็นค่าออบเจ็กต์ที่ต้องแปลงให้เป็น string ก่อนจะนำไปใส่ใน value string จากนั้นเรานำค่าใน key string และ value string ออกแสดงบนจอภาพดังนี้

Contents of the hashtable:

Key: 415 value: Henry

Key: 999 value: Joe

Key: 389 value: Mary

ต่อไปโปรแกรมจะลบหน่วยข้อมูลที่มี key เป็น 415 โดยเรียก remove() อันเป็นเมธอดของคลาส Hashtable หลังลบแล้วโปรแกรมจะแสดงขนาดของตารางแฮช และข้อมูลของตารางแฮชบนจอภาพดังนี้

Removing an entry from the hashtable:

Size of hashtable after removing node: 2

Contents of the hashtable:

Key: 999 value: Joe

Key: 389 value: Mary

ซอร์โค้ดสมบูรณ์ในภาษาจาวา

/////////// HashtableDemo.cpp ///////////////////
import java.lang.*;
import java.text.*;
import java.util.*;
public class HashtableExample{
    public static void main(String[] args){
        Hashtable hashtable = new Hashtable();
        String key;
        String value;
        Key = 9341594;
        Value = 93Mary94;
        If (!hashtable.containsKey(key)){
            System.out.println(93Adding node 96 key: 93 + key + hashtable.put(key, value);
        }

        Key = 9341594;
        Value = 93Henry94;
        If (!hashtable.containsKey(key)){
            System.out.println(93Adding node 96 key: 93 + key + hashtable.put(key, value);
        }

        Key = 9399994;
        Value = 93joe94;
        If (!hashtable.containsKey(key)){
            System.out.println(93Adding node 96 key: 93 + key + hashtable.put(key, value);
        }
        System.out.println(93\nRetrieving a value out of the hashtable:94);
        value = (String)Hashtable.get(9341594);
        if(value != null){
            System.out.println(93Found key: 93 + key + 93 value: 93 + value);
        }
        System.out.println(93\nSide of hashtable before removing nodes: 93 + hashtable.size());
        System.out.println(93\nContents of the hashtable: 93);
        displayEntries(hashtable);
        System.out.println(93\nRemoving an entry from the hashtable: 93);
        hashtable.remove(9341594);
        System.out.println(93\nSize of hashtable after removing node: 93 + hashtable after removing node: 93 + hashtable.size());
        System.out.println(93\nContents of the hashtable: 93);
        displayEntries(hashtable);
        }
        private static void displayEntries(Hashtable hashtable){
            Set keys = hashtable.keySet();
            Iterator ii = key.iterator();
            while(ii.hasNext()){
                String key = (String)ii.next();
                String value = (String)hashtable.get(key);
                System.out.println(93key: 93 + key + 93\tvalue: 93 + value);
        }
    }
}

 สรุปท้ายบทความ

การเขียนโปรแกรมตารางแฮชด้วยภาษา C++ และภาษาจาวาไม่ยากมากจนเกินไปหากท่านเข้าใจหลักการทำงานของมัน ในภาษา C# จะง่ายพอๆ กับจาวา เพราะ .NET Framework มีคลาสไลบรารีเกี่ยวกับ Hash มาให้อย่างสมบูรณ์เราจึงสามารถเรียกใช้ได้ง่ายๆ หากท่านต้องการอ่านบทความเทคนิกการเขียนโปรแกรมอื่นๆ ของผู้เขียนกรุณาติดตามในนิตยการ Windows IT Pro ได้ทุกเดือน

Advertisements

Hash table ( ตารางแฮช ) 1/2

ไปหน้าแรก | สารบัญ | Laploy.comระเบียนบทความ | บทความจากลาภลอย

สร้าง Hash table ( ตารางแฮช )

ด้วยภาษา C++ และจาวา ตอน 1

ท่านผู้อ่านท่านหนึ่งตั้งกระทู้ถามมาในเว็บบอร์ด laploy.com ว่าดังนี้ “อยากทราบว่า Hashtable ทำงานอย่างไร ใช้กับอะไรบ้าง ในกรณีไหนที่ควรใช้หรือไม่ควร อยากให้อาจารย์ช่วยอธิบายให้หน่อยครับ ขอพระคุณครับ” ผมจึงตัดสินใจเขียนรับใช้เป็นบทความที่คุณจะได้อ่านต่อไปนี้

 ตารางแฮช หรือ Hash table คือโครงสร้างข้อมูลชนิดหนึ่ง (โครงสร้างข้อมูลแบบอื่นๆ คือ สแตก คิว ลิงค์ลิสต์และอื่นๆ อีกมาก) คำว่าตารางแฮช หรือ Hash table เป็นหนึ่งในคำศัพท์ด้านการเขียนโปรแกรมที่มีการทำงานไม่ตรงตามชื่อของมันเลย คำว่า hash (คลุกเคล้า) อันที่จริงหมายถึง mishmash (ไม่คลุกเคล้า) และคำว่าตารางปรกติจะหมายถึงชุดข้อมูลที่จัดเป็นคอลัมน์และแถว ตารางซึ่งเก็บข้อมูลแบบปะปนกันยุ่งดูเหมือนจะไม่มีประโยชน์ในการนำไปใช้งาน แต่อันที่จริงมิได้เป็นเช่นนั้น นักเขียนโปรแกรมสามารถนำตารางแฮชมาใช้เก็บข้อมูลขนาดใหญ่ได้อย่างมีประสิทธิภาพ ในบทความนี้คุณจะได้เรียนวิธีสร้างและวิธีนำตารางแฮชมาประยุกต์ใช้งาน

 

กายวิภาคของตารางแฮช

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

อ็อบเจ็กต์ต่างๆ จะถูกเก็บโดยใช้สมาชิกโครงสร้างข้อมูลเพียงตัวเดียว การจะเลือกโครงสร้างข้อมูลแบบใดขึ้นอยู่กับลักษณะของการประยุกต์ใช้งาน ตารางแฮชคือโครงสร้างข้อมูลที่ใช้เพื่อเก็บอ็อบเจ็กต์ที่มีความสัมพันธ์ระหว่าง key และ value

ตารางแฮชเป็นอาร์เรย์ที่เก็บพอยน์เตอร์ที่ชี้ไปยังข้อมูล (array of pointer ต่อไปจะเรียกย่อว่า AP) ข้อมูลจะอยู่ในรูปแบบของโครงสร้างข้อมูลที่ผู้ใช้นิยามขึ้นเอง(User Define Structure ต่อไปจะเรียกย่อๆ ว่า UDS) อันประกอบด้วยส่วนต่างๆ สามส่วน คือ key, value และพอยน์เตอร์ที่ทำหน้าที่ชี้ไปยัง UDS ลำดับถัดไปในตารางแฮช เรามีความจำเป็นต้องใช้พอยน์เตอร์นี้เฉพาะในกรณีที่มีการชนกันของข้อมูลดังจะได้อธิบายต่อไปในบทความนี้

key ทำหน้าที่แยกแยะความแตกต่างให้แก่ value ข้อมูลใน UDS แต่ละตัวต้องมี key ที่ไม่ซ้ำกัน value คือข้อมูลที่คู่กันกับ key ซึ่งภายในตารางแฮชหนึ่งๆ อาจมีข้อมูลซ้ำกันได้ คิดเสียว่า key เป็นหมายเลขประจำตัวนักเรียน ซึ่งอยู่คู่กันกับชื่อนักเรียน นักเรียนแต่ละคนย่อมได้รับหมายเลขประจำตัวไม่ซ้ำกัน แต่นักเรียนบางคนชื่ออาจเหมือนกันก็ได้

สิ่งที่ทำให้ตารางแฮชมีความน่าสนใจคือวิธีที่นักเขียนโปรแกรมกำหนด UDS ให้แก่หน่วยในอาร์เรย์ (Array Element ต่อไปจะเรียกย่อๆ ว่า AE) ของตารางแฮช โดยโปรแกรมจะนำ key ของ UDS มา “คลุกเคล้ากัน” ก่อนจะนำไปตัดสินใจว่า AE ใดจะคู่กับ UDS ใด

ถ้าคุณอ่านย่อหน้าบนแล้วงงก็ไม่ต้องใจเสีย ที่คุณงงเป็นเรื่องธรรมดาเพราะหลักการนี้ไม่ใช่แนวคิดที่เห็นภาพได้ชัดเจนในทันที ลองดูตัวอย่างต่อไปนี้แล้วคุณจะเข้าใจ รูปที่ 1 แสดงข้อมูลสามตัวในตารางแฮช ภาพนี้ลดทอนความซับซ้อนลงโดยให้กล่องหนึ่งกล่องเป็นตัวแทนอินสแตนซ์ของ UDS ที่มีค่า key/value เก็บอยู่ ส่วนนิยามโดยละเอียดของ USD ของจริงๆ เราจะว่ากันอีกทีภายหลังในบทความนี้

 

รูปที่ 1 ตารางแฮชคืออาร์เรย์ซึ่งหน่วยข้อมูลชี้ไปยังข้อมูลในโครงสร้างข้อมูลที่ผู้ใช้กำหนดเอง

 

นอกจากนั้นรูปที่ 1 ยังแสดงอาร์เรย์ซึ่งเป็นตัวแทนของตารางแฮช ให้สังเกตว่า AE แต่จะตัวจะชี้ไปยัง UDS ซึ่งทำหน้าเป็นตัวเก็บข้อมูลจริงๆ ในตารางแฮช

UDS แต่ละตัวจับคู่กับ AE โดยใช้ค่า key ใน UDS โดยค่าของ key จะถูกนำมาแปลงเป็นดรรชนีของอาร์เรย์โดยใช้กระบวนการแปลงที่เรียกว่าการแฮช (hashing) ผลลัพธ์ที่ได้คือค่าแฮช (hash value) จะถูกนำไปใช้เป็นค่าดรรชนีของอาร์เรย์ที่ชี้ไปยัง UDS ตัวที่ค่า key ถูกทำการแฮช ค่าดรรชนีของอาร์เรย์ที่เห็นในรูปที่ 1ก็เป็นค่าที่ได้จากกระบวนการแฮชที่กล่าวมานี้

ขอให้คิดเสียว่าการแฮชคือการทำให้ได้ค่าดรรชนีที่จับคู่ค่า key ของ UDS กับ AE ของอาร์เรย์ในตารางแฮช ส่วนการแฮชทำอย่างไรคุณจะได้เรียนในหัวข้อต่อๆ ไปในบทความนี้

เทคนิคการแฮชทำให้ได้ค่าดรรชนีแบบกระจายตัวเสมอกัน (even distribution index) ช่วยให้การค้นข้อมูลทำได้เร็วกว่าการค้นข้อมูลจากดรรชนีที่เรียงลำดับ โดยใช้คำหรือชื่อที่เรียงตัวกันในรูปแบบที่ทำนายได้ เพราะเมื่อเรานำบิตต่างๆ ที่ประกอบเป็นคำหรือชื่อมาสลับสับเปลี่ยนคลุกเคล้ากัน โปรแกรมจะไม่มองบิตต่างๆ ว่าเป็นชุดตัวอักษรอีกต่อไป แต่จะมองว่าเป็นบิตที่รวมตัวกันอย่างสุ่มเสี่ยง (randomize) ซึ่งการค้นจะทำได้อย่างมีประสิทธิภาพมากกว่า

กระบวนการแฮชเป็นกลวิธีความเร็วสูงอย่างหนึ่งที่นำค่า key ที่เรียงตัวตามค่าตัวเลขหรือตัวอักษรมาทำให้เป็นค่าสุ่มปลอม (pseudo random) หากค่า key เป็นชื่อ ตัวอักษรบางตัวจะปรากฏบ่อยกว่าตัวอักษรบางตัว หรือลำดับอักษรบางแบบจะปรากฏบ่อยกว่าบางแบบ การแฮชจะโยกย้ายบิตต่างๆ เพื่อทำให้มันกลายเป็นค่าสุ่มอย่างหลอกๆ และมีอัตราการกระจายตัวที่สม่ำเสมอมากขึ้น ซึ่งจำเป็นต้องใช้เพื่อการค้นตารางแฮชได้อย่างรวดเร็ว

ผลลัพธ์ที่ได้จากการแฮชคือจำนวนที่ไม่มีนัยสำคัญอย่างแท้จริง (no real significance number) ซึ่งเราจะนำมาใช้เป็นค่าดรรชนีในการ เรียก-เก็บ ข้อมูลที่สัมพันธ์กันกับค่าในตารางแฮช ดรรชนีที่เห็นในรูปที่ 1 คือค่าแฮชที่ได้จากการแฮช key ใน UDS ที่คู่กันกับค่าใน AE ของตารางแฮช

 

ปัญหาของการแฮช

เทคนิคการแฮชมิได้สมบูรณ์พร้อม บางครั้งจะเกิดปัญหาขึ้นเมื่อนำค่า key ที่แตกต่างกันสองอันมาทำการแฮชแล้วเกิดได้ผลลัพธ์เป็นค่าแฮชซ้ำกัน และถูกเก็บไว้ใน AE เดียวกัน นักเขียนโปรแกรมได้คิดค้นทางแก้เพื่อจัดการกับความขัดแย้งนี้ได้หลายวิธี

วิธีสามัญในการแก้ไขความขัดแย้งนี้คือการจับหน่วยข้อมูลที่มีค่าแฮชซ้ำกันไปใส่ในลิงค์ลิสต์ ยกตัวอย่างเช่นเรามีข้อมูลสองหน่วยที่ key ของมันให้ค่าแฮชที่ซ้ำกันและถูกนำไปเป็นไว้ใน AE เดียวกันอย่างที่เห็นในรูปที่ 2

 

รูปที่ 2 เมื่อมีค่าแฮชซ้ำกันเราจะใช้ลิงค์ลิสต์มาเชื่อมโยงโครงสร้างข้อมูลที่ผู้ใช้กำหนดเอง

 

เนื่องจากข้อมูลสองชุดจะอยู่ใน AE เดียวกันไม่ได้นักเขียนโปรแกรมจึงสร้างลิงค์ลิสต์ และให้ AE ชี้ไปที่ UDS อันแรก ส่วน UDS อันที่สองไม่ต้องมี AE มาชี้ แต่จะใช้วิธีเชื่อมโยงกับ UDS อันแรกด้วยตัวเชื่อมโยงของลิงค์ลิสต์

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

นักเขียนโปรแกรมแก้ปัญหานี้โดยเขียนโปรแกรมให้อ่านค่า key ของหน่วยข้อมูลตัวแรกและนำมาเปรียบเทียบกับข้อมูลที่ต้องการค้นหา หากไม่ตรงกันโปรแกรมจะเลื่อนไปตรวจดูตัวต่อไปในลิงค์ลิสต์ และทำซ้ำอย่างนี้จนกว่าจะพบข้อมูลที่ต้องการ หรือไม่ก็เคลื่อนไปจนถึงสุดลิงค์ลิสต์

 

การพัฒนาตารางแฮช

การสร้างและประยุกต์ใช้งานตารางแฮชมีสองขั้นตอน ขั้นตอนแรกคือการนิยาม UDS ซึ่งคล้ายการนิยาม node ของลิงค์ลิสต์ ขั้นตอนที่สองคือการสร้างคลาส Hashtable ซึ่งคลาส Hashtable จะทำหน้าที่ประกาศอินสแตนซ์ของ UDS และนิยามสมาชิกข้อมูลและฟังก์ชันสมาชิกที่จำเป็นต้องใช้เพื่อเพิ่ม ลบ และจัดการกับข้อมูลในตารางแฮช

 

คลาส Hashtable

ขั้นตอนแรกของการนำตารางแฮชมาใช้ในโปรแกรมของเราคือการสร้างนิยาม UDS และคลาส Hashtable ที่เราจะใช้ทำงานกับตารางแฮชในโปรแกรมของเรา เราจะเริ่มโดยการนิยาม UDS ดังนี้

typedef struct Metadata{
    struct Metadata(char* key, char* value){
        strcpy(this->key, key);
        strcpy(this->value, value);
        next = NULL;
    }
    char key[SIZE_KEY];
    char value[SIZ_VALE];
    struct Metadata* next;
}METADAT;

 

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

โครงสร้างข้อมูล metadata มีสมาชิกสามตัว สมาชิกตัวแรกคืออาร์เรย์แบบ char ชื่อ key ซึ่งเราใช้ทำหน้าที่เก็บ key ที่เป็นคู่ค่า key/value สมาชิกตัวที่สองคืออาร์เรย์แบบ char ชื่อ value ซึ่งเราใช้ทำหน้าที่เก็บตัวข้อมูลที่เป็นคู่ค่า key/value สมาชิกตัวสุดท้ายชื่อ next เป็นพอยน์เตอร์ทำหน้าที่ชี้ไปยังโครงสร้าง metadata ตัวต่อไป ทำให้โปรแกรมสามารถหาโครงสร้างข้อมูลที่เชื่อมโยงกันจากค่าดรรชนีที่กำหนดได้

ขนาดของอาร์เรย์แบบ char ทั้งสองตัวถูกกำหนดโดยค่าจากการ #define ซึ่งเรานิยามมาโครไว้สองตัวคือ SIZE_KEY และ SIZE_VALUE นิยามของมาโครสองตัวนี้จะอยู่ในไฟล์ HashTable.h

คุณคงสังเกตว่าในนิยาม structure มี constructor อยู่ด้วยเพื่อให้โปรแกรมประยุกต์สามารถกำหนดค่าเริ่มต้นให้แก่ค่า key และ value ของ structure ได้ ซึ่งค่าเหล่านี้จะถูกนำไปใส่ในอาร์เรย์แบบ char ที่เกี่ยวข้อง นอกจากนั้นมันยังทำหน้าที่กำหนดค่าเริ่มต้นให้พอยน์เตอร์ next ที่จะชี้ไปยัง node ถัดไปในลิงค์ลิสต์ดังคุณจะได้เห็นวิธีทำต่อไปในบทความนี้

เมื่อเรานิยามโครงสร้าง metadata แล้ว ต่อไปเราจะนิยามคลาส Hashtable ซึ่งมีหน้าที่ประกาศอินสแตนซ์ของโครงสร้าง metadata และนิยามฟังก์ชันต่างๆ ที่จะนำไปใช้จัดการโครงสร้าง metadata

นิยามของคลาส Hashtable เป็นดังนี้

class Hashtable{
    private:
    int tablesize;
    METADATA** table;
    int size;
    METADATA* current_entry;
    Int current_index;
    Long hashString(char* key);
    METADATA* find(char* key);
public:
    Hashtable(int tablesize = DEFAULT_TABLESIZE);
    virtual ~Hashtable();
    bool put(char* key, char* value);
    bool get(char* key, char* value);
bool contains(char* key);
bool remove(char* key);
void removeAll();
int getSize();
void initIterator();
bool hashNext();
void getNextKey(char* key);
};

 

เราจัดรูปแบบคลาส Hashtable ให้แบ่งเป็นสองส่วนคือส่วน private access specifier และส่วน public access specifier ในบริเวณ private access specifier เราจะใช้เพื่อเก็บสมาชิกข้อมูลห้าตัว และฟังก์ชันสมาชิกอีกสองตัว

สมาชิกข้อมูลตัวแรกเป็นชนิดเลขจำนวนเต็มชื่อ Tablesize ซึ่งภายหลังเราจะกำหนดให้เป็นข้อมูลขนาดของอาร์เรย์พอยน์เตอร์ที่ใช้เก็บหน่วยข้อมูลต่างๆ ในตารางแฮช สมาชิกตัวต่อไปคือพอยน์เตอร์ที่ชี้ไปยังพอยน์เตอร์ (Pointer to Pointer ต่อไปจะเรียกย่อๆ ว่า PTP) ที่เราตั้งชื่อว่า table ทำหน้าที่เป็นอาร์เรย์พอยน์เตอร์ของ metadata ซึ่งเก็บข้อมูลในตารางแฮช ข้อมูลแต่ละหน่วยที่อยู่ใน table คือพอยน์เตอร์ชี้ไปลิงค์ลิสต์ของหน่วยข้อมูลนั้นๆ เบื้องต้นเราใส่ค่า NULL ไว้เพื่อแสดงว่าที่ดรรชนีนี้ยังไม่มีหน่วยข้อมูล

สมาชิกข้อมูลตัวที่สามของคลาส Hashtable เป็นตัวแปรแบบเลขจำนวนเต็มชื่อ size ซึ่งภายหลังเราจะนำค่าจำนวนหน่วยข้อมูลในตารางแฮชมาเก็บ สมาชิกสองตัวสุดท้ายคือ current_entry และ current_index สมาชิก current_entry คือพอยน์เตอร์ที่ชี้ไปยังหน่วยข้อมูลปัจจุบันภายในโครงสร้าง metadata ส่วน current_index คือเลขจำนวนเต็มทำหน้าที่แสดงค่า key ปัจจุบัน ทั้งคู่จะถูกใช้ซ้ำกับทุกหน่วยข้อมูลในตารางแฮช

นอกจากสมาชิกข้อมูลแล้ว ในพื้นที่ส่วน private access specifier ของคลาส Hashtable ยังมีฟังก์ชันสมาชิกอีกสองตัวคือ hashString() และ find() ฟังก์ชัน hashString() ทำหน้าที่รับค่า key ที่เป็นค่าชุด key/value แล้วส่งกลับมาเป็นรหัสที่แฮชแล้ว รหัสแฮชที่ส่งกลับมานี้เป็นค่าดรรชนีของหน่วยข้อมูลที่อยู่ภายในตารางแฮช ดรรชนีคืออาร์เรย์ของ metadata พอยน์เตอร์ ฟังก์ชัน find() ทำหน้าที่ค้นตารางแฮชตาม key ที่กำหนดแล้วคืนผลลัพธ์มาเป็นพอยน์เตอร์ที่ชี้ไปยังโครงสร้าง metadata ที่มีค่าตรงกับ key นั้น ฟังก์ชันทั้งสองนี้จะถูกฟังก์ชันอื่นเรียกใช้อีกทีดังจะพรรนณารายละเอียดต่อไปภายหลังในบทความนี้

ในส่วน public access specifier ของคลาส Hashtable จะประกอบไปด้วยฟังชันสมาชิกต่างๆ ที่เราจำเป็นต้องใช้เพื่อสร้างและจัดการกับตารางแฮช ซึ่งจะได้อธิบายรายละเอียดของแต่ละตัวต่อไปในบทความนี้

 

Constructor และ Destructor

Constructor ของคลาส Hashtable ทำหน้าที่กำหนดค่าเริ่มต้นให้แก่สมาชิกข้อมูลและสร้างตารางแฮชดังแสดงในตัวอย่างโปรแกรมข้างล่าง เมื่อโปรแกรมสร้างอินสแตนซ์ของคลาส Hashtable ขนาดของอาร์เรย์พอยน์เตอร์ (tablesize) จะถูกส่งมายัง constructor ค่าที่จะส่งให้ constructor ต้องเป็นเลขจำนวนเต็ม ซึ่งจะถูกนำไปกำหนดให้เป็นค่าของสมาชิกข้อมูล tablesize ในคลาส Hashtable

Constructor ทำหน้าที่จองหน่วยความจำให้แก่อาร์เรย์พอยน์เตอร์ metadata ซึ่งเราจะใช้เก็บข้อมูลตารางแฮช อาร์เรย์นี้จะถูกกำหนดค่าจากสมาชิก table ของคลาส คุณได้เรียนไปก่อนหน้านี้แล้วว่าสมาชิกข้อมูล table คืออาร์เรย์พอยน์เตอร์ที่ชี้ไปยังโครงสร้าง metadata

 

รูปที่ 3 constructor ประกาศพอยน์เตอร์อาร์เรย์ซึ่งหน่วยข้อมูลแต่ละตัวของอาร์เรย์ชี้ไปยังอินสแตนซ์ของโครงสร้าง metadata

 

ทันทีที่อินสแตนซ์ถูกสร้างขึ้น constructor จะใช้ลูป for เพื่อกำหนดค่าเริ่มต้นให้แก่หน่วยต่างๆ ในตารางให้เป็นค่า NULL สมาชิกข้อมูล size ถูกกำหนดให้เป็นศูนย์เพื่อเป็นเครื่องแสดงว่าไม่มีหน่วยข้อมูลอยู่ภายในตาราง อย่างไรก็ดีคุณอาจเพิ่มขนาดของตารางได้โดยส่งผ่านค่า tablesize มายัง constructor รูปที่ 3 แสดงตารางแฮชที่มีข้อมูลห้าหน่วย

Hashtable(int tablesize){
    Size = 0;
    this->tablesize = tablesize;
    table = new METADATA* [tablesize];
    for(int I=0; I<tablesize; I++){
        table[I] = NULL;
    }
}

เมธอด destructor ของคลาส Hashtable แสดงในตัวอย่างโปรแกรมถัดไป มันมีหน้าที่สองอย่าง อย่างแรกคือเรียกฟังก์ชัน removeAll() เพื่อลบหน่วยข้อมูลทั้งหมดออกจากตารางแฮช อย่างที่สองคือลบพอยน์เตอร์ที่ใช้ชี้ไปยังตารางโดยใช้ตัวกระทำ delete

~Hashtable(){
    removeAll();
    delete[] table;
}

 

การแทรกหน่วยข้อมูลใหม่

คุณแทรกหน่วยข้อมูลใหม่เข้าไปในตารางแฮชได้โดยเรียกใช้ฟังก์ชัน put() ซึ่งเป็นฟังก์ชันที่เรียกใช้จากโปรแกรมประยุกต์ได้โดยตรงเพราะเรานิยามไว้ในส่วน public access specification ของคลาส Hashtable

ดังคุณจะเห็นในตัวอย่างโปรแกรมถัดไปว่าฟังก์ชัน put() ต้องการอาร์กิวเมนต์สองตัว ตัวแรกเป็นพอยน์เตอร์แบบ char ชื่อ key ทำหน้าที่เก็บค่า key ของหน่วยข้อมูลใหม่ อาร์กิวเมนต์ตัวที่สองชื่อ value เป็นพอยน์เตอร์แบบ char อีกเหมือนกัน ทำหน้าที่ชี้ไปยังข้อมูลของหน่วยข้อมูลใหม่ ฟังก์ชัน put() จะคืนค่าบูลีนเป็นจริงหากแทรกหน่วยข้อมูลใหม่ได้สำเร็จมิฉะนั้นจะคืนค่ามาเป็นเท็จ

ดังที่คุณคงจะจำได้ว่าค่า key แต่ละตัวจะต้องไม่ซ้ำกัน ดังนั้นก่อนการแทรกข้อมูลใหม่เราจึงต้องตรวจสอบก่อนว่ามีค่า key ที่จะแทรกอยู่ในตารางแฮชแล้วหรือไม่ ซึ่งทำได้โดยเรียกใช้ฟังก์ชัน find() และส่งค่า key ไปให้ฟังก์ชัน find()

ค่า key ที่เก็บอยู่ในตารางแฮชนั้นเหมือนกันทุกประการกับค่า key ที่เราส่งให้ฟังก์ชัน find() ค่าแฮชคือตัวกำหนดว่าจะนำมันไปใส่ใน bucket ใด และใช้ฟังก์ชัน find() เพื่อเปรียบเทียบค่า key ในตารางกับค่า key ที่ต้องการ คุณจะได้เรียนรายละเอียดของฟังก์ชัน find() ภายหลังในบทความนี้

หากไม่พบ key ที่ต้องการฟังก์ชัน find() จะคืนค่ามาเป็น NULL มิฉะนั้นมันจะคืนค่ามาเป็นพอยน์เตอร์ที่ชี้ไปยังโครงสร้าง metadata ตัวที่มีค่า key นั้น หากฟังก์ชัน find() ไม่ได้คืนค่ามาเป็น NULL แสดงว่ามี key นั้นอยู่ในตารางแฮชแล้วฟังก์ชัน put() จะคืนค่ามาเป็นเท็จ

อย่างไรก็ดีหากฟังก์ชัน find() คืนค่ามาเป็น NULL หน่วยข้อมูลใหม่จะถูกนำไปใส่เพิ่มในตารางโดยสร้างอินสแตนซ์ใหม่ของโครงสร้าง metadata แล้วกำหนดค่าให้เป็น key และ value ของข้อมูลหน่วยใหม่ และนำค่าอ้างอิงไปเก็บไว้ในพอยน์เตอร์ชื่อ entry

ต่อไปเป็นการเรียกฟังก์ชัน hashString() เพื่อส่งค่า key ไปยังข้อมูลหน่วยใหม่ ฟังก์ชัน hashString() ทำหน้าที่แฮชค่า key ซึ่งจะถูกนำไปใช้เป็นดรรชนีของอาร์เรย์สำหรับหน่วยต่างๆ ในตารางแฮช ค่าแฮชจะถูกนำไปใส่ในตัวแปรแบบเลขจำนวนเต็มชื่อ bucket คุณจะได้เรียนการทำงานของฟังก์ชัน hashString() ในบทความนี้ภายหลัง

ค่าจำนวนเต็มใน bucket จะถูกนำไปใช้เป็นดรรชนีของอาร์เรย์ชื่อ data ซึ่งเป็นสมาชิกข้อมูลของคลาส Hashtable ดังที่คุณคงยังจำได้จากหัวข้อที่แล้วในบทความนี้ว่า data คืออาร์เรย์ของพอยน์เตอร์ที่ชี้ไป metadata ซึ่งทำให้ table[bucket] ชี้ไปยังส่วนในตารางที่จะถูกกำหนดให้เป็นหน่วยข้อมูลใหม่

ก่อนที่จะกำหนดส่วนในตารางให้เป็นหน่วยข้อมูลใหม่ หน่วยข้อมูลที่อยู่ใน bucket ขณะนั้นจะต้องถูกนำไปใส่ในอินสแตนซ์ถัดไปของโครงสร้าง metadata ที่ชื่อ entry ซึ่งเราประกาศไว้ในฟังก์ชัน put() หลังการกำหนดค่านี้ หน่วยข้อมูลใหม่จะถูกนำไปใส่ในส่วน table[bucket] ของตารางแฮช ผลกระทบที่เกิดขึ้นคือหน่วยข้อมูลใหม่นี้จะกลายเป็นหน่วยข้อมูลแรกในลิงค์ลิสต์ของอาร์เรย์ตำแหน่งนี้ หากที่ดรรชนีนี้ยังไม่มีหน่วยข้อมูล ค่าของดรรชนีจะเป็น NULL จึงทำให้พอยน์เตอร์ next ของหน่วยข้อมูลใหม่เป็น NULL ซึ่งก็ถูกต้องเพราะหน่วยข้อมูลใหม่เป็นเพียงหน่วยข้อมูลเดียวภายในลิงค์ลิสต์

 

รูปที่ 4 นี่คือสิ่งที่เกิดขึ้นหลังการใส่หน่วยข้อมูลใหม่เข้าไปในตารางแฮช

 

ต่อไปฟังก์ชัน put() จะเพิ่มค่าตัวแปร size ซึ่งเป็นสมาชิกข้อมูลของคลาส Hashtable เพื่อเป็นเครื่องชี้ว่าบัดนี้หน่วยข้อมูลใหม่ได้ถูกเพิ่มเข้ามาในตารางแฮชแล้ว แล้วฟังก์ชัน put() จึงคืนค่าบูลีน เป็นจริง รูปที่ 4 แสดงภาพตารางแฮชเมื่อเราส่งค่า 111 เป็น key และ Bob เป็น value ให้แก่ฟังก์ชัน put()

bool put(char* key, char* value){
    if(find(key) != NULL){
        return false;
    }
    METADATA* entry = new METADATA(key, value);
    int bucket = hashString(key);
    entry->next = table[bucket];
    table[bucket] = entry;
    size++;
    return true;
}

 

การดึงข้อมูล

คุณสามารถดึงข้อมูลที่เก็บอยู่ในตารางแฮชโดยเรียกฟังก์ชัน get() ดังแสดงในตัวอย่างโปรแกรมถัดไป ฟังก์ชัน get() ต้องการอาร์กิวเมนต์สองตัว ตัวแรกคือพอยน์เตอร์แบบ char ที่ชี้ไปยัง key ของหน่วยข้อมูลที่คุณต้องการดึง หากพบ key ที่ต้องการในตารางแฮชฟังก์ชัน get() คัดลอกค่าของหน่วยข้อมูลจากตารางแฮชมายังพอยน์เตอร์ char แล้วมันจะคืนค่าบูลีนเป็นจริง หากไม่พบ key ที่ต้องการในตารางแฮชฟังก์ชัน get()จะคืนค่าบูลีนเป็นเท็จ

ฟังก์ชัน get() ค้นในตารางแฮชโดยเรียกฟังก์ชัน find() และส่งค่า key ที่มันได้รับเป็นอาร์กิวเมนต์ตัวแรกไปให้ฟังก์ชัน find() ฟังก์ชัน find() จะแฮชค่า key ก่อนที่จะค้นในตารางแฮช หากฟังก์ชัน find() พบข้อมูลที่หามันจะคืนค่ามาเป็นค่าอ้างอิงตำแหน่งของโครงสร้าง metadata ที่เก็บ key นั้น ถ้าหาไม่เจอก็จะคืนค่ามาเป็น NULL

ฟังก์ชัน get() จะนำค่าอ้างอิงที่ได้รับจากฟังก์ชัน find() มาใส่ใน temp ซึ่งเป็นพอยน์เตอร์ที่ใช้ชี้ไปยังโครงสร้าง metadata ฟังก์ชัน get() จะตรวจสอบว่าค่าใน temp เป็น NULL หรือไม่ หากใช่มันจะกำหนดให้ AE แรกของอาร์กิวเมนต์ value มีค่าเป็น NULL character (ทำให้ value เป็น string ที่ว่างเปล่า) แล้วฟังก์ชัน get() จะคืนค่าบูลีนเป็นเท็จเพื่อเป็นเครื่องแสดงว่าไม่มี key นั้นในตารางแฮช

หาก temp ไม่เป็น NULL แสดงว่าฟังก์ชัน find() พบค่า key ในตารางแฮช และมันได้คืนค่ามาเป็นตำแหน่งอ้างอิงไปยัง metadata ที่มีหน่วยข้อมูล value ของหน่วยข้อมูลที่ต้องการคัดลอกไปใส่ในอาร์กิวเมนต์ value แล้ว จากนั้นฟังก์ชัน get() จะคืนค่าบูลีนเป็นจริง

bool get(char* key, char* value){
    METADATA* temp = find(key);
    if(temp == NULL){
        value[0] = 9192;
        return false;
    }else{
        strcpy(value, temp->value);
        return true;
    }
}

 

ฟังก์ชัน find()

เราต้องเรียกใช้ฟังก์ชัน find() หลายครั้งจากในฟังก์ชันต่างๆ ดั้งนั้นเราจะมาดูการทำงานโดยละเอียดของฟังก์ชัน find() กันหน่อย อย่างที่คุณคงจำได้ว่าจุดประสงค์ของฟังก์ชัน find() คือค้นตารางแฮชเพื่อหา key หากพบ key ฟังก์ชัน find() จะคืนค่ามาเป็นค่าอ้างอิงโครงสร้าง metadata ที่มี key และ value ที่คู่กันเก็บอยู่ หากไม่พบ key ฟังก์ชัน find() จะคืนค่ามาเป็น NULL

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

ค่าแฮชที่ได้จากฟังก์ชัน hashString() จะถูกนำไปใส่ในตัวแปรแบบเลขจำนวนเต็มชื่อ bucket ซึ่งถูกใช้เป็นดรรชนีเพื่อระบุหน่วยในอาร์เรย์ table ซึ่งก็คือตารางแฮช ค่า AE ของมันคือค่าอ้างอิงไปยังโครงสร้าง metadata ซึ่งจะถูกนำมากำหนดให้กับพอยน์เตอร์ temp ของโครงสร้าง metadata

ตราบเท่าที่พอยน์เตอร์ temp ไม่เป็น NULL ฟังก์ชัน find() จะใช้ฟังก์ชัน strcmp() เพื่อเปรียบเทียบค่า key ในโครงสร้าง metadata กับค่า key ที่ฟังก์ชัน find() ได้รับมาเป็นอาร์กิวเมนต์ และ temp จะถูกใช้เป็นเงื่อนไขของคำสั่ง while ซึ่งทำงานเป็นวนรอบเพื่อการค้นหาใน node ทั้งหมดของลิงค์ลิสต์ หากไม่มีข้อมูลที่ตรงกัน สมาชิกตัวถัดไปของโครงสร้าง metadata ที่ชี้ด้วย temp จะถูกนำไปใส่ใน temp และฟังก์ชัน find() จะเปรียบเทียบค่า key ต่อไป

หากตรวจสอบทั้งตารางแฮชแล้วไม่พบค่าที่ตรงกัน ฟังก์ชัน find() จะคืนค่ามาเป็น NULL

METADATA* find(char* key){
    int bucket = hashString(key);
    METADATA* temp = table(bucket);
    while(temp != NULL){
        if(strcmp(key, temp->key) == 0){
            return temp;
        }
        temp = temp->next;
    }
    return NULL;
}

 

ฟังก์ชัน contains()

หน้าที่ของฟังก์ชัน contains() คือตรวจสอบว่ามีค่า key อยู่ในตารางแฮชหรือไม่ อย่างที่คุณเห็นในนิยามถัดไป ฟังก์ชัน contains() เป็นฟังก์ชันที่สร้างง่ายไม่ซับซ้อน แต่กระนั้นก็มีบทบาทสำคัญในการทำงานกับตารางแฮช

อย่างที่คุณเรียนไปแล้วในตอนต้นบทว่า key ในตารางแฮชต้องไม่ซ้ำกัน ฟังก์ชัน contains() ทำให้โปรแกรมประยุกต์สามารถแน่ใจได้ว่า key ทั้งหมดแตกต่างกันโดยการตรวจสอบว่ามี key ใหม่ที่จะเพิ่มเข้าไปอยู่ในตารางแฮชแล้วหรือยัง

ฟังก์ชัน contains() ต้องการอาร์กิวเมนต์เพียงตัวเดียว ซึ่งก็คือค่าอ้างอิงถึง key ที่คุณต้องการตรวจสอบว่ามีอยู่ในตารางแฮชหรือไม่ ฟังก์ชัน contains() จะคืนค่าบูลีนเป็นจริงหากมี key นั้นแล้ว หรือคืนค่าบูลีนเป็นเท็จหากยังไม่มี key นั้นในตารางแฮช

ฟังก์ชัน contains() ตรวจสอบว่ามี key ที่ต้องการแล้วหรือยังโดยเรียกฟังก์ชัน find() แล้วส่งค่า key ไปให้ คุณเรียนไปแล้วในหัวข้อก่อนหน้านี้ในบทความนี้ว่าฟังก์ชัน find() จะคืนค่ามาเป็นค่าอ้างอิงไปยังโครงสร้าง metadata หรือไม่ก็ NULL ฟังก์ชัน contains() จึงคอยตรวจดูว่าฟังก์ชัน find() คืนค่าอะไรมา

หากฟังก์ชัน find() คืนค่า NULL มาฟังก์ชัน contains() ก็จะคืนค่าบูลีนเป็นเท็จ มิฉะนั้นหากฟังก์ชัน find() คืนค่าอ้างอิงไปยังโครงสร้าง metadata ที่มี key ฟังก์ชัน contains() ก็จะคืนค่าบูลีนเป็นจริง

bool contains(char* key){
    if(find(key) == NULL){
        return false;
    }else{
        return true;
    }
}

 

การลบหน่วยข้อมูล

คุณต้องเรียกใช้ฟังก์ชัน remove() เมื่อใดก็ตามที่โปรแกรมของคุณต้องการลบหน่วยข้อมูลออกจากตารางแฮช ฟังก์ชัน remove() ซึ่งแสดงในตัวอย่างโปรแกรมถัดไปต้องการอาร์กิวเมนต์เพียงตัวเดียว ซึ่งเป็นค่า key ของหน่วยข้อมูลที่คุณต้องการลบออกจากตารางแฮช หากพบหน่วยข้อมูลที่ตรงกับค่า key และลบได้สำเร็จ ฟังก์ชัน find() จะคืนค่าบูลีนเป็นจริงหรือมิฉะนั้นจะคืนค่าบูลีนเป็นเท็จ

ฟังก์ชัน remove() จะแฮชค่า key ก่อนทำการค้นหาและลบหน่วยข้อมูล โดยการเรียกฟังก์ชัน hashString() แล้วส่งค่า key ซึ่งเป็นอาร์กิวเมนต์ที่ฟังก์ชัน remove() ได้รับมา ฟังก์ชัน hashString() จะคืนค่ามาเป็นค่าแฮชของ key นั้น ซึ่งจะถูกนำไปใส่ในตัวแปรชนิดเลขจำนวนเต็มชื่อ bucket

ตัวแปร bucket ถูกใช้เป็นค่าดรรชนีของอาร์เรย์ table ซึ่งเป็นสมาชิกข้อมูลของคลาส Hashtable ค่า table[bucket] คือค่าอ้างอิงไปยังหน่วยข้อมูลในตารางแฮช ซึ่งเก็บลิงค์ลิสต์ที่จะต้องถูกค้น ค่านี้จะถูกนำไปใส่ในพอยน์เตอร์ temp เพื่อการหาซ้ำในลิงค์ลิสต์

จากนั้นฟังก์ชัน remove() จะตรวจดูว่ามีหน่วยข้อมูลอยู่หรือไม่โดยเปรียบเทียบพอยน์เตอร์ temp กับค่า NULL หากพบว่าเป็น NULL มันจะส่งคืนค่าบูลีนเป็นเท็จไปให้บรรทัดคำสั่งที่เรียกฟังก์ชัน remove() หาก temp ไม่ใช่ NULL แสดงว่าในลิงค์ลิสต์มีหน่วยข้อมูลอยู่อย่างน้อยหนึ่งหน่วย ฟังก์ชัน remove() จะตรวจสอบว่าหน่วยข้อมูลที่จะลบเป็นหน่วยใดในลิงค์ลิสต์ของตารางแฮช

ขั้นแรกฟังก์ชัน remove() จะตรวจสอบว่าหน่วยข้อมูลเป็น node แรกของลิงค์ลิสต์หรือไม่โดยใช้ฟังก์ชัน strcmp() เพื่อตรวจสอบค่า key ของหน่วยข้อมูลกับค่า key ที่ฟังก์ชัน remove() ได้รับมา หากมีค่าตรงกันฟังก์ชัน strcmp() จะคืนค่ามาเป็นศูนย์ ฟังก์ชัน remove() ก็จะรู้ว่าหน่วยข้อมูลที่ต้องการลบคือ node แรกของลิงค์ลิสต์ หากหน่วยข้อมูลไม่ใช่ node แรกของลิงค์ลิสต์ ฟังก์ชัน remove() จะต้องทำงานซ้ำเพื่อตรวจสอบ node ที่เหลือในลิงค์ลิสต์เพื่อค้นหาหน่วยข้อมูลที่ต้องการลบ

หากหน่วยข้อมูลที่ต้องการลบคือ node แรกในลิงค์ลิสต์ ฟังก์ชัน remove() จะดำเนินการสลับตำแหน่ง node ภายในลิงค์ลิสต์ อย่างที่คุณเรียนไปแล้วก่อนหน้านี้ในบทความนี้ว่า metadata ประกอบด้วยสามส่วนคือ key, value และ next ที่ใช้อ้างอิงไปยังหน่วยข้อมูลถัดไปในลิงค์ลิสต์

ค่าตำแหน่งของ node ที่ชี้โดย next จะถูกนำไปเก็บไว้ใน table[bucket] ซึ่งก่อนหน้านี้เก็บค่าอ้างอิงไปยังหน่วยข้อมูลที่กำลังจะถูกลบออกจากตารางแฮช การทำเช่นนี้เป็นเหตุให้หน่วยข้อมูลถัดไปกลายเป็น node แรกของลิงค์ลิสต์เพราะว่าหน่วยข้อมูลปัจจุบันเป็นหน่วยข้อมูลอันแรกของลิงค์ลิสต์

เมื่อสลับที่แล้วฟังก์ชัน remove() จะใช้ตัวกระทำ delete เพื่อยกเลิกการจองหน่วยความจำของหน่วยข้อมูลปัจจุบัน ซึ่งถูกชี้โดยพอยน์เตอร์ temp หน่วยข้อมูลจึงถูกลบออกไปจากตารางแฮชด้วยการทำงาน ณ จุดนี้ จากนั้นฟังก์ชัน remove() จะลดค่า size ลงหนึ่งเพื่อให้สัมพันธ์กับการที่ข้อมูลถูกลบไป แล้วคืนค่าบูลีนเป็นจริงเพื่อเป็นเครื่องแสดงว่าประสบความสำเร็จในการลบหน่วยข้อมูล

หากหน่วยข้อมูลไม่ใช่ node แรกของลิงค์ลิสต์ ฟังก์ชัน remove() จะต้องขยับไปตรวจดู node ถัดไปในลิงค์ลิสต์ ซึ่งทำได้โดยนำค่า next ของโครงสร้าง metadata ซึ่งเป็นหน่วยข้อมูลถัดไป มาใส่ในพอยน์เตอร์ temp_next ตราบเท่าที่พอยน์เตอร์ temp_next ไม่ใช่ NULL ฟังก์ชัน remove() จะเรียกฟังก์ชัน strcmp() เพื่อเปรียบเทียบค่า key ที่ฟังก์ชัน remove() ได้รับมาเป็นอาร์กิวเมนต์ กับค่า key ที่เป็นสมาชิกของโครงสร้างข้อมูลที่ temp_next ชี้อยู่

หากพบว่าตรงกัน จะมีการสลับที่ node เหมือนการสลับที่ node เมื่อพบค่าที่ตรงกันกับ node แรกของลิงค์ลิสต์ หากไม่ตรงกันค่า next จะถูกนำไปใส่ในพอยน์เตอร์ temp_next แล้วดำเนินการค้าหาต่อไป หากหาค้นหาจนเกลี้ยงลิงค์ลิสต์แล้วไม่พบค่า key ฟังก์ชัน remove() จะคืนคาบูลีนเป็นเท็จ

bool remove(char* key){
    int bucket = hashString(key);
    METADATA* temp = table[bucket];
    if(temp == NULL){
        return false;
    }else if(strcmp(key, temp->key) == 0) {
        table[bucket] = temp->next;
        delete temp;
        size--;
        return true;
    }else{
        METADATA* temp_next = temp->next;
        while(temp_next != NULL){
            if(strcmp(key, temp_next->key) == 0){
                temp->next = temp_next->next;
                delete temp_next;
                size--;
                return true;
            }
            temp = temp->next;
            temp_next = temp_next->next;
        }
    }
    return false;
}

อีกวิธีหนึ่งที่จะลบรายการจากตารางแฮชคือใช้ฟังก์ชัน removeAll() ดังแสดงในตัวอย่างโปรแกรมถัดไป ฟังก์ชันนี้จะลบข้อมูลทั้งหมดออกจากตารางแฮช ฟังก์ชัน removeAll() ทำเช่นนั้นได้โดยใช้ลูป for วนซ้ำไปแต่ละหน่วยในตาราง ในแต่ละหน่วยจะมีคำสั่ง while คอยวนลบหน่วยในลิงค์ลิสต์ เมื่อลบออกไปได้หนึ่งหน่วยแล้วลูป for จะเคลื่อนไปยังหน่วยถัดไปไล่ลบไปเช่นนี้จนกระทั้งหน่วยข้อมูลทุกหน่วยถูกลบออกจากตารางแฮช

การทำงานเริ่มโดยประกาศตัวแปรชื่อ temp ซึ่งเป็นพอยน์เตอร์ทำหน้าที่ชี้ไปยังโครงสร้าง metadata จากนั้น temp จะได้รับค่าอ้างอิงไปยังหน่วยข้อมูลแรกในอาร์เรย์ table ของตารางแฮช

ตราบเท่าที่พอยน์เตอร์ temp ไม่มีค่าเป็น NULL ฟังก์ชัน removeAll() จะใส่ค่าอ้างอิงไปยังหน่วยข้อมูลถัดไปของหน่วยข้อมูลปัจจุบัน (คือค่าจากสมาชิกข้อมูล next ของโครงสร้าง metadata) ค่า key และ value จะถูกแสดงบนจอภาพก่อนที่หน่วยข้อมูลตัวที่ถูกชี้ด้วยพอยน์เตอร์ temp จะถูกลบด้วยตัวกระทำ delete

หน่วยข้อมูลที่ถูกชี้ด้วยพอยน์เตอร์ next จะถูกนำไปใส่ในพอยน์เตอร์ temp แล้วฟังก์ชัน removeAll() จะกลับไปทำงานที่ต้นลูป while เพื่อลบหน่วยข้อมูลถัดไปในตารางแฮช การทำงานจะดำเนินไปเช่นนี้จนพอยน์เตอร์ temp มีค่าเป็น NULL ซึ่งหมายถึงตารางแฮชว่างเปล่า (เมื่อ temp มีค่าเป็น NULL แสดงว่าเสร็จไปหนึ่งลิงค์ลิสต์ จาวาจะกลับออกไปที่ลูปนอกเพื่อดำเนินการกับลิงค์ลิสต์ตัวต่อไป) สุดท้ายฟังก์ชัน removeAll() จะกำหนดค่า size ซึ่งเป็นสมาชิกของคลาส Hashtable ให้เป็นศูนย์เพื่อเป็นเครื่องแสดงว่าบัดนี้ไม่มีหน่วยข้อมูลในตารางแฮชแล้ว

void removeAll(){
    for(int i=0; i<tablesize; i++){
        METADATA* temp = table[i];
        while(temp != NULL){
            METADATA* next = temp->next
            cout << 93Removing node 96key:94 << temp->key << 93\t94 << temp->value << endl;
            delete temp;
            temp = next;
        }
    }
    size = 0;
}

 

ฟังก์ชัน getSize()

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

คุณอาจสงสัยว่าทำไมต้องนิยามฟังก์ชัน getSize() ในเมื่อเราจะให้โปรแกรมประยุกต์อ่านค่าสมาชิกข้อมูล size โดยตรงก็ได้ คำตอบคือเพื่อสร้างบูรณภาพของข้อมูล เพราะหากเราย่อมให้โปรแกรมเข้าถึงสมาชิกข้อมูล size ได้โดยตรง คำสั่งในโปรแกรมอาจไปเปลี่ยนแปลงค่า size ให้เป็นค่าที่ไม่ถูกต้องได้ ดังนั้นการกำจัดการเข้าถึงไว้เฉพาะฟังก์ชันสมาชิกของคลาสจึงสามารถป้องกันความผิดพลาดในลักษณะนี้ได้

int getSize(){
    return size;
}

 

ฟังก์ชัน hashString()

ฟังก์ชัน hashString() เป็นอีกฟังก์ชันหนึ่งที่ฟังก์ชันต่างๆ ในคลาส Hashtable เรียกใช้เมื่อต้องการแปลงค่า key ให้เป็นค่าแฮช ฟังก์ชัน hashString() ต้องการอาร์กิวเมนต์เพียงตัวเดียวคือพอยน์เตอร์ชนิด char ซึ่งชี้ไปยังค่า key ที่จะแฮช ผลการแฮชที่ได้จะส่งคืนมาให้เป็นข้อมูลชนิด long

นิยามของฟังก์ชัน hashString() แสดงในตัวอย่างโปรแกรมถัดไป การแฮชเริ่มโดยตรวจสอบขนาดของ key โดยใช้ฟังก์ชัน strlen() ขนาดจะถูกนำไปใส่ในตัวแปรชนิดจำนวนเต็มชื่อ n และเราประกาศตัวแปรแบบ long ชื่อ h ซึ่งเรากำหนดค่าเริ่มต้นเป็นศูนย์ เราจะใช้ตัวแปรนี้เก็บพักค่าระหว่างทำการประมวลผลค่าแฮช

ขั้นตอนการแฮชเป็นการทำงานกับค่า key ในระดับบิต เป็นการทำให้ค่า key กลายเป็นค่าสุ่ม ฟังก์ชัน hashString() จะใช้ลูป for วนทำงานกับแต่ละตัวอักษรของ key ในการวนแต่ละรอบตัวแปร h จะถูกเลื่อนบิต (h<<2) และบิตของตัวอักษรปัจจุบันของ key จะถูกนำไปใส่แทนที่บิตที่ถูกเลื่อน แล้วนำผลลัพธ์ที่ได้ไปเก็บในตัวแปร h กระบวนการนี้ดำเนินไปเรื่อยจนกระทั่งตัวอักษรของ key ถูกแฮชจนครบทุกตัว

ถัดไปฟังก์ชัน hashString() จะทำการคำนวณเพื่อหาค่าโมดูลัส (การหารแบบเอาเศษ) ของค่าแฮชขั้นสุดท้าย (h) กับค่าขนาดของตาราง (h % tablesize) ค่าที่ได้จากการทำโมดูลัสอาจเป็นได้ทั้งบวกและลบ อย่างไรก็ดีค่าแฮชต้องเป็นจำนวนบวกเท่านั้น ดังนั้นฟังก์ชัน hashString() จึงแปลงค่าที่ได้จากการทำโมดูลัสให้เป็นค่าสัมบูรณ์เสียก่อน (abs(h % tablesize))

long hashString(char* key){
    int n = strlen(key);
    long h = 0;
    for(int i=0; i<n; i++){
        h = (h << 2) + key[i];
    }
    return abs(h % tablesize);
}

 

หมายเหตุ : เป้าหมายของการแฮชคือการนำชุดข้อมูลที่เป็น key มาแปลงเป็นค่าดรรชนีซึ่งมีลำดับการกระจายตัวที่เหมาะสม แม้นักเขียนโปรแกรมหลายคนจะเห็นพ้องกันว่าขนาดของแฮชควรเป็นจำนวนเฉพาะ (prime number) แต่ยังมีขั้นตอนวิธี (Algorithm) ในการแฮชอีกหลายแบบ อย่างไรก็ดีขั้นตอนวิธีในการหาค่าแฮชทุกแบบจะมีการเลื่อนบิตร่วมอยู่ด้วยเสมอ

ไม่มีฟังก์ชันใดให้ค่าแฮชที่ดีพร้อม ฟังก์ชันแฮชบางแบบก็เหมาะกับข้อมูลบางแบบ ตามปรกติแล้วนักเขียนโปรแกรมจะทดสอบฟังก์ชันแฮชหลายๆ แบบกับชุดข้อมูลที่ต้องการก่อนที่จะเลือกฟังก์ชันแฮชที่ให้ประสิทธิภาพสูงสุด

 

เรื่อง Hash table ยังไม่จบ (ที่ท่านอ่านไปนี้เป็นครึ่งแรก) เนื่องจาก Blog ของ Space โพสหนึ่งตอนยาวมากไม่ได้ ผู้เขียนจึงแบ่งออกเป็นสองโพส กรุณาติดตามอ่านส่วนที่เหลือในโพสถัดไป

ตอนต่อไป : ฟังก์ชัน initIterator() ส่วนอื่นๆ ของคลาส Hash table และการสร้างในจาวา

สร้างหุ่นยนต์ใหม่จากซากไดร์ฟเก่า

ไปหน้าแรก | สารบัญ | Laploy.comระเบียนบทความ | บทความจากลาภลอย


วันนี้ค้นซีดีเก่าๆ เจอบทความที่เขียนไว้หลายปีแล้ว เอามา post ไว้ที่นี่เพื่อรำลึกอดีต

1)แคสเตอร์ 2)แผ่นวงจรควบคุม 3)สเต็ปมอร์เตอร์ 4)สายต่อไปพีซี 5)สายไฟเลี้ยง

 

สร้างหุ่นยนต์ใหม่จากซากไดร์ฟเก่า

ดิสก์ไดร์ฟเก่าที่เสียแล้วอย่าโยนลงถังขยะ เพราะดัดแปลงเป็นหุ่นยนต์ เพื่อการศึกษาวิธีสร้างหุ่นยนต์ ด้วยงบเพียง 233 บาท

ผมอ่านบทความเรื่อง “เปลี่ยน Floppy Drive ให้เป็นรถประป๋อง” ในควิกพีซีฉบับที่ 124 แล้วชอบใจมาก เพราะผมมีดิสก์ไดร์ฟที่ใช้ไม่ได้แล้วอยู่หลายตัว แกะออกมาจากเครื่องนานแล้วยังไม่ได้โยนทิ้ง กะว่าว่างๆ จะเอามาทำอะไรเล่น พออ่านบทความจบจึงค้นออกมาตรวจสอบ ผมพบว่านอกจาก DC มอเตอร์แล้ว ในดิสก์ไดร์ฟยังมีอุปกรณ์ที่มีประโยชน์อีกหลายอย่าง อาธิ ไมโครสวิชท์ โฟโต้เซนเซอร์ LED สเต็ปปิ้งมอเตอร์ และวงจรขับสเต็ปปิ้ง

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

การสร้างหุ่นหนูน้อยด้วยดิสก์ไดร์ฟเก่า คุณควรจะรู้ข้อมูลในหัวข้อต่างๆ ดังนี้

  • เรื่องของหุ่นหนู
  • ฟล็อปปิดิสก์ไดร์ฟ
  • สเต็ปปิ้งมอเตอร์
  • พอร์ทขนาน
  • การประกอบตัวหุ่น
  • แคสเตอร์ – วิธีทำ
  • การประกอบวงจร
  • ซอฟท์แวร์

 

เรื่องของหุ่นหนู

หุ่นหนู (Micro mouse robot) เป็นหุ่นที่มีการจัดแข่งขันกันทั่วโลกมาหลายสิบปีแล้ว กติกาคือปล่อยหุ่นเข้าไปในเขาวงกต ให้หุ่นหาทางออกมาให้ได้เองโดยเจ้าของไม่ต้องยุ่งเกี่ยว หุ่นตัวไหนใช้เวลาน้อยที่สุดก็ชนะไป ดังนั้นหุ่นจะต้องมีฮาร์ดแวร์และซอฟท์แวร์ที่สมบุรณ์ในตัว มีแบตตารีและมีตัวตรวจจับประเภทต่างๆ จึงจะมีปัญญาสำรวจทำแผนที่และคิดหาทางออกมาเองได้

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

หุ่นหนูของจริง

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

 

ดิสก์ไดร์ฟ

ซากดิสก์ไดร์ฟที่ผมมีเป็นไดร์ฟสามนิ้วครึ่ง 1.4M ยี่ห้อ TEAC รุ่น FD-235HF ถ้าคุณมียี่ห้ออื่นรุ่นอื่นก็ใช้ได้ เพราะดิสก์ไดร์ฟของพีซีเป็นมาตรฐานเดียวกันหมดอยู่แล้ว แต่ถ้าคุณไม่มีดิสก์ไดร์ฟที่เสียแล้วสักตัว ไม่ต้องเอาของใหม่มาแกะครับ ลองไปเดินตามศูนย์ไอทีเดี๋ยวก็ได้ติดไม้ติดมือมา ไดร์ฟที่เสียแล้วร้านค้าจะกองใส่ลังวางไว้หน้าร้าน ราคาตัวหนึ่งไม่ควรจะเกินสามสิบบาท

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

ดิสก์ไดร์ฟขนาดสามนิ้วครึ่ง

1)ช่องด้านหน้า 2)หัวอ่านเขียน 3)ขาเสียบแหล่งจ่ายไฟ 4)สเต็ปมอร์เตอร์ 5)ขาคอนเน็ตเตอร์

ในบทความนี้ผมจะอ้างถึงไดร์ฟสามนิ้วครึ่งเป็นหลัก เพราะเดี๋ยวนี้หาไดร์ฟห้านิ้วยาก แต่ถ้าคุณหาได้ก็ใช้ได้เหมือนกัน และดีกว่าด้วย เพราะมอเตอร์มีแรงบิดสูงกว่า แต่ให้สังเกตุว่าไดร์ฟห้านิ้วรุ่นเก่าๆ จะใช้ไฟ 12 โวลท์นะครับ ไม่ใช่ 5 โวลท์อย่างที่ระบุในบทความนี้

 

สเต็ปปิ้งมอเตอร์

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

สเต็ปปิ้งมอเตอร์ (บางคนเรียกสเต็ปเปอร์มอเตอร์ หรือสเต็ปมอเตอร์ก็ว่า) เป็นมอเตอร์พิเศษที่ไม่เหมือนมอเตอร์ธรรมดา เพราะภายในมีขดลวดหลายขด (สองถึงสี่ขด) เมื่อป้อนไฟเข้าไปก็จะหมุนเพียงแค่จังหวะเดียวเท่านั้น (จังหวะหนึ่งอยู่ในระยะ 1 ถึง 8 องศา ขึ้นอยู่กับชนิดของมอเตอร์) สเต็ปปิ้งมอเตอร์มีสองแบบ คือแบบ ไบโพลา และยูนิโพลา สเต็ปปิ้งมอเตอร์ในดิสก์ไดร์ฟเป็นแบบไบโพลา มีทอร์ค (แรงบิด) ต่ำ ความเร็วรอบสูงสุดอยู่ที่ 300 รอบต่อวินาที ใช้ไฟเลี้ยง 5 โวลท์ กินกระแสร์ประมาณ 200 มิลลิแอมป์

 

สเต็ปมอร์เตอร์ขนาดใหญ่

สเต็ปมอร์เตอร์ที่ได้จากซากไดร์ฟ

 

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

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

 

ฮาร์ทแวร์ของพอร์ทขนาน

พอร์ทขนานหรือพรินท์เตอร์พอร์ท เป็นช่องทางอีกแห่งหนึ่ง ที่คอมพิวเตอร์ใช้ติดต่อกับโลกภายนอก เดิมถูกออกแบบมาให้ใช้ต่อกับเครื่องพิมพ์ ต่อมาปรากฏว่ามี่ผู้ผลิตฮาร์ดแวร์สร้างอุปกรณ์อย่างอื่นให้ต่อกับพอร์ทขนานได้ด้วย เช่นอุปกรณ์เก็บข้อมูลของไอโมเมก้าที่ต่อได้ทั้ง SCSI และพอร์ทขนาน

การใช้งานพอร์ทขนานไม่จำเป็นต้องรู้เรื่องวงจรภายในของมัน เพียงแต่รู้การกำหนดหน้าทีขาของมันก็พอ หัวต่อสำหรับพอร์ทขนานเป็นชนิด DB25 มียี่สิบห้าขา โดยขาที่ 1 ถึงขาที่ 17 ใช้เพื่อการสื่อสาร ส่วนขาที่เหลือเป็นกราวน์ทั้งหมด

หัวต่อ DB25 ที่อยู่ด้านหลังเครื่องเป็นคอนเนคเตอร์ตัวเมีย ชนิด D-Sub ส่วนหัวต่อ DB25 ที่มาจากเครื่องพิมพ์เป็นตัวผู้ที่เราจะต้องใช้หนึ่งตัว ผมเองหาแกะเอาจากสายเครื่องพิมพ์เก่าที่ไม่ได้ใช้แล้ว

การเรียงขาของคอนเนคเตอร์พอร์ทขนาน

คอนเนคเตอร์พอร์ทขนาน

สัญญาญที่ออกมาจากพอร์ทขนานเป็นสัญญาณที่อยู่ในระดับ TTL คืออยู่ในช่วง 0 ถึง 2.4 โวลท์ จายกระแสร์ ได้ประมาณ 2 มิลลิแอมป์ เราจึงนำมาเชื่อมต่อกับวงจรขับสเต็ปปิ้งมอเตอร์ของฟล็อปปิดิสก์ได้โดยตรง แต่น่าเสียดายที่พอร์ทขนานไม่มีขาจ่ายไฟ 5 โวลท์ที่เราต้องใช้ป้อนให้วงจร ดังนั้นจึงไม่มีทางเลือกนอกจากต้องใช้ไฟ 5 โวลท์จากหัวจ่ายไฟให้ดิสก์ไดร์ฟของเพาเวอร์ซัพพลายในเครื่องพีซี

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

ไปหาคอนเนคเตอร์ DB-25 มาบัดกรีสายซะ

 

ซอฟท์แวร์ของพอร์ทขนาน

ก่อนที่จะพูดถึงการเขียนโค้ดผมอยากจะอธิบายสภาพของพอร์ทขนานโดยย่อพอเข้าใจ ในทางซอฟท์แวร์พอร์ทขนานจะมีสามส่วน คือ

ส่วนข้อมูล : มีแปดเส้น จึงรับ-ส่งข้อมูลได้พร้อมๆ กันทีละแปดบิท ในงานควบคุมง่ายๆ เราใช้เฉพาะส่วนข้อมูลก็เหลือเพือแล้ว

ส่วนควบคุม : มีสี่เส้น ใช้ส่งสัญญาณออกไปสู่โลกภายนอก (เช่นสัญญาณบอกเครื่องพิมพ์ให้เลื่อนกระดาษ)

ส่วนรับสถานะ : มีห้าเส้น รับข้อมูลจากภายนอกทั้งห้าเส้น แม้พอร์ทขนานในเครื่องคอมพิวเตอร์ยุคปัจจุบัน จะสามารถรับหรือส่งข้อมูลก็ได้ แต่ส่วนรับสถานะถือว่าเป็นอินพุทมาตรฐานของพอร์ทขนานมาตั้งแต่แรก

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

ตัวเลขนี้คือค่ามาตรฐานของพอร์ทขนาน LPT1 ในเครื่องทั่วๆ ไป เครื่องของคุณอาจจะถูกตั้งค่าไว้ต่างไปจากนี้ก็เป็นได้ วิธีเข้าไปตรวจสอบดูว่าพอร์ทขนานของเครื่องคุณมีแอดเดสรเท่าไหร่แน่ให้ตรวดูโดยกด Start/programs/accessories/system tools/system information ดูในหัวข้อ I/O ในรายการบรรทัดที่เขียนว่า LTP

 

ดิสก์ไดร์ฟเพาเวอร์คอนเนคเตอร์

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

คุณจะตัดสายแล้วนำสายไฟมาพันเข้าไปเลยก็ได้ แต่ผมไม่อยากตัดสาย เพราะเผื่อว่าเลิกทดลอง จะได้นำไปต่อกับดิสก์ไดร์ฟตามเดิม เนื่องจากหัวต่อนี้เป็นตัวเมีย ผมจึงใช้วิธีนำสายไฟมาเสียบเข้าไปในรูแล้วเอาเทปพันไว้ จากนั้นดูให้แน่ใจว่าจะไม่หลุดออกมาง่ายๆ แค่นี้ก็เป็นอันเสร็จ ปลายอีกด้านหนึ่งระวังอย่าให้ช็อตกัน ไม่งั้นเดี๋ยวเพาเวอร์ซัพพลายพังขึ้นมาจะยุ่งกันใหญ่

หัวต่อไฟเลี้ยงของฟล็อปปิดิสก์ไดร์ฟ

แสดงวิธีต่อสายเพื่อนำไฟ 5 โวลท์จากคอมพิวเตอร์มาใช้

 

การประกอบตัวหุ่น

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

ถ้าคุณมีดิสก์ไดร์ฟแบบไม่มีฝาอลูมิเนียมก็คงต้องหาวัสดุอย่างอื่นมาทดแทน ลองหาดูรอบๆ บ้าน เช่นอาจจะใช้แผ่นฟิวเจอร์บอร์ด หรือใช้ไม้อัดสำหรับเด็กทำการฝีมือก็เก๋ดีเหมือนกัน

แผ่นอลูมิเนียมที่แกะออกมาจากดิสก์ไดร์ฟตรงขอบมีสันพับขึ้นมา แต่ความสูงของสันไม่มากพอที่จะจับยึดมอเตอร์ไว้ได้ ผมจึงต้องหาแผ่นพลาสติกมาเสริมเข้าไป ผมใช้เศษแผ่นพีวีซีที่เจอหล่นอยู่ข้างโต๊ะทดลอง ถ้าคุณไม่มีแผ่นพีวีซีก็ไม่จำเป็นต้องไปซื้อ ใช้ฝากล่องซีดีนี่แหละมาตัดให้มีขนาดประมาณ 2×2 นิ้ว ใช้ได้ดีพอๆ กัน

แผ่นอะลูมีเนียมที่แกะออกมาจากไดร์ฟ

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

ตอนที่ผมไปบ้านหม้อเพื่อซื้อไอซีมาทำวงจรสั่งการพีซีผ่านสายโทรศัพท์ (จะเขียนรับใช้ในโอกาสหน้าครับ) เผอิญเจอร้านขายอุปกรณ์ทำเครื่องบินบังคับวิทยุ เห็นมีล้อเครื่องบินขาย หุ้มล้อเป็นโฟม แกนล้อเป็นพลาสติก น้ำหนักเบามาก ราคาไม่แพง เลยซื้อมาสองอัน เก็บเอาไว้เฉยๆ นานแล้ว วันนี้เลยถือโอกาสนำมาทำเป็นล้อหุ่นหนูเสียเลย พบว่าเหมาะเหมือนกัน เพราะมีขนาดและน้ำหนักกำลังดี ถ้าคุณหาล้อแบบนี้ไม่ได้ จะแกะล้อออกมาจากรถเด็กเล่นที่เสียแล้วก็ได้ หรือใช้ฝาพลาสติกที่มีเส้นผ่าศูนย์กลางสักหกนิ้วก็พอจะใช้ได้ ข้อสำคัญคือต้องเป็นอะไรที่เบาๆ หน่อย

 

แคสเตอร์

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

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

แคสเตอร์ที่ประกอบขึ้นเองจากวัสดุรอบตัว

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

 

วิธีทำแคสเตอร์

โชคร้ายหน่อยที่แคสเตอร์ซึ่งมีขายตามร้านทั่วไปไม่สามารถนำมาใช้กับหุ่นหนูน้อยได้ เพราะมีขนาดใหญ่และมีน้ำหนักมากเกินไป แต่โชคดีที่เราสามารถประกอบแคสเตอร์ขึ้นเองได้โดยใช้อุปกรณ์ราคาถูกที่หาได้ไม่ยาก

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

1)น็อตตัวเมีย 2)แหวนเล็ก 3)แหวนใหญ่ 4)ด้ามปากกา 5)แหวนกลาง 6)สปริงที่แกะมาจากปากกาลูกลื่น 7)น็อตยาวยึดกับล้อยางของเครื่องเล่นเทป

ซอฟท์แวร์

ต่อไปเป็นวิธีสร้างซอฟท์แวร์เพื่อควบคุมหุ่นยนต์นี้ด้วยภาษา VB ถ้าคุณเป็นโปรแกรมเมอร์ภาษาแอสเซมบลี้ หรือภาษาซี คุณย่อมจะรู้วิธีเขียนโปรแกรมควบคุมพอร์ทขนานอยู่แล้ว ผมไม่จำเป็นต้องอธิบายอะไรมาก แต่ถ้าคุณไม่ใช่โปรแกรมเมอร์ แต่เป็นนักเล่นคอมฯ เหมือนผม เราย่อมรู้สึกสบายใจกว่าที่จะสร้างแอพลิเกชันโดยเปิด VB ขึ้นมา ลากๆ หยอดๆ และเขียนโค้ดแทรกลงไปนิดๆ หน่อยๆ

ผมมีข่าวดีกับข่าวร้ายจะบอก เอาข่าวร้ายก่อนก็แล้วกัน ข่าวร้ายคือยอดชายนายบิลเกสต์ แกไม่ยักใส่คำสั่งควบคุมพอร์ทขนานไว้ในภาษา VB ดังนั้นงานนี้จึงต้องซิกแซกกันหน่อย ส่วนข่าวดีคือมีคนสร้างโปรแกรมย่อย (ซับรูทีน) สำหรับ VB ใช้ควบคุมพอร์ทขนานมาให้เราใช้ฟรีๆ เยอะแยะ ในบทความนี้ผมจะใช้ไลบรารีของบริษัท SoftCircuits Programming (www.softcircuits.com) ผู้แสนจะใจกว้าง ก่อนจะลงมือเขียนโปรแกรมก็จัดการไปดาวน์โหลดมาเสียก่อน (หน้าสำหรับดาวน์โหลดอยู่ที่ http://www.softcircutis.com/sw_tools.htm)

ถ้าคุณใช้ VB เวอร์ชัน 1,2,3,4 แบบ 16 บิท ให้ดาวน์โหลดไฟล์ชื่อ vbasm.zip เมื่อนำมาคลายออกจะได้ไฟล์ชื่อ vbasm.dll ให้นำไปใส่ใน windows/system

ส่วนผู้ที่ใช้ VB 4 หรือเวอร์ชันใหม่กว่านั้นในแบบ 32 บิท ให้ดาวน์โหลดไฟล์ชื่อ win95io.zip เมื่อนำมาคลายออกจะได้ไฟล์ชื่อ vb95io.dll แล้วนำไปใส่ใน windows/system เช่นเดียวกัน

ส่วนผู้ที่ใช้ NT ต้องขอแสดงความเสียใจด้วย เพราะ DLL นี้ไม่สามารถทำงานใน Windows NT ได้ ส่วนจะใช้ใน Windows Xp ได้หรือไม่นั้นยังไม่ได้ทดสอบ

เมื่อติดตั้งไฟล์ .dll กันเรียบร้อยแล้ว ก่อนจะใช้งานได้ ในส่วนหัวของโปรแกรม VB (ในโมดูลพับลิก) จะต้องประกาศการใช้งานไลบรารีพวกนี้เสียก่อนโดยพิมพ์คำสั่งดังต่อไปนี้เข้าไป

 

การประกาศฟังค์ชัน API

สำหรับ VB 16 บิท

Declare Function vbInp Lib "VBASM.DLL" (ByVal nPort As Integer) As Integer
Declare Sub vbOut Lib "VBASM.DLL" (ByVal nPort As Integer, ByVal nData As Integer)

สำหรับ VB 32 บิท

Declare Sub vbOut Lib "WIN95IO.DLL" (ByVal nPort As Integer, ByVal nData As Integer)

Declare Sub vbOutw Lib "WIN95IO.DLL" (ByVal nPort As Integer, ByVal nData As Integer)

Declare Function vbInp Lib "WIN95IO.DLL" (ByVal nPort As Integer) As Integer

Declare Function vbInpw Lib "WIN95IO.DLL" (ByVal nPort As Integer) As Integer

แล้วเราจะส่งค่าไปที่พอร์ทได้อย่างไร? ง่ายมาก เพราะตอนนี้ VB ของเรามีคำสั่งพิเศษเพิ่มขึ้นมาสองคำสั่งคือ vbInp และ vbOut เรียบร้อยแล้ว (เพราะไลบรารี่ที่เราติดตั้งลงไป) คำสั่ง vbInp ทำหน้าที่รับข้อมูลจากพอร์ทขนาน และ vbOut ทำหน้าที่ส่งข้อมูลออกไปยังพอร์ทขนาน ทั้งสองคำสั่งมีรูปแบบการใช้งานดังนี้

vbOut [หมายเลขพอร์ท], [ค่าที่ต้องการส่งไป]

ดังนั้นถ้าคุณต้องการให้พอร์ทขนาน (ส่วนข้อมูล) เป็น 0 หมดทุกเส้น (ไม่มีไฟ หรือเท่ากับกราวน์) คุณก็บอกว่า

vbOut 8880, 0

ดังนั้นถ้าคุณต้องการให้พอร์ทขนาน (ส่วนข้อมูล) เป็น 1 หมดทุกเส้น (มีไฟ) คุณก็บอกว่า

vbOut 8880, 255

ถ้าต้องการให้พอร์ทขนาน (ส่วนข้อมูล) เป็นมีไฟและไม่มีไฟสลับกันในแต่ละเส้น คุณก็บอกว่า

vbOut 8880, 170 ‘ 170 เท่ากับ 10101010 ในฐาน 2

ถ้าคุณไม่สันทัดเรื่องการแปลงเลขฐานสิบไปเป็นฐานสอง (เหมือนผม) ก็ใช้วิธีเปิดเครื่องคิดเลขใน Windows แล้วเลือก View/Scientific คุณจะสามารถแปลเลขฐานต่างๆ กลับไปกลับมาได้อย่างคล่องแคล่วจนน่าทึ่ง

ในเมื่อเราหาวิธีส่งค่าออกไปที่พอร์ทขนานได้แล้ว ทีนี้ก็มาลองเขียนโปรแกรมทดสอบดูก่อนว่าพอร์ทขนานของเราทำงานถูกต้องหรือไม่ และสายที่ต่อไว้ถูกต้องดีหรือไม่ ให้ต่อสายพอร์ทขนานกับวงจรขับ LED แล้วเขียนโปรแกรมดังนี้

 

โปรแกรมทดสอบพอร์ทขนาน

‘ ใส่โค้ดบรรทัดนี้ไว้ในส่วนประกาศของโปรเจค (ในโมดูลพับลิค)

Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

‘ สร้างปุ่ม command button ขึ้นมาหนึ่งปุ่ม ตั้งชื่อว่า cmdFlash แล้วใส่โค้ดดังนี้ไว้

Private Sub cmdFlash_Click()

    vbOut 8880, 0

    Call Sleep(1000)

    vbOut 8880, 255

    Call Sleep(1000)

End sub

เมื่อวิ่งโปรแกรมนี้คุณจะเห็น LED ดับหนึ่งวินาทีและติดหนึ่งวินาทีสลับกันไป ถ้าเราให้โปรแกรมทำงานโดยไม่หน่วงเวลาเลย โปรแกรมจะทำงานเร็วมากจะไม่เห็นการกระพริบ แต่เนื่องจาก VB ไม่มีคำสั่งหน่วงเวลา เราจึงต้องเรียกฟังค์ชัน sleep ซึ่งเป็นฟังชัน API มาใช้ 1000 จะเท่ากับ 1 วินาที 500 จะเท่ากับครึ่งวินาที

ลองวิ่งโปรแกรมแล้วตรวจสอบดูว่ามีหลอดไหนไม่ติดหรือไม่ ถ้ามีแสดงว่าต่อสายผิด หรือสายขาด (หรือ LED เสีย)

วงจรง่ายๆ เพื่อใช้ทดสอบโปรแกรม

โปรแกรมไฟวิ่ง

ต่อไปลองโปรแกรมนี้

Private Sub cmdFlash_Click()

    vbOut 8880, 170

    Call Sleep(1000)

    vbOut 8880, 85

    Call Sleep(1000)

End sub

โปรแกรมนี้จะทำให้ LED ติดและดับสลับดวงกัน ถ้าลดค่าหน่วงเวลาลงเหลือ 500 ความเร็วในการสลับจะหลอกตา ทำให้ดูเหมือนเป็นไฟวิ่ง

เมื่อตรวจสอบดูจนแน่ใจแล้วว่าฮาร์ดแวร์ และซอฟท์แวร์ถูกต้อง ขั้นต่อไปก็จะมาควบคุมการเคลื่อนที่ของเจ้าหุ่นกันเสียที การจะให้หุ่นเคลื่อที่ก็ทำได้โดยบังคับให้สเต็ปปิ้งมอเตอร์หมุนไป โดยมีลักษณะการเคลื่อนที่ที่เป็นไปได้ดังนี้

  1. ไปข้างหน้า : มอเตอร์หมุนไปข้างหน้าพร้อมๆ กันทั้งสองล้อ
  2. ถอยหลังตรง : มอเตอร์หมุนกลับหลังพร้อมๆ กันทั้งสองล้อ
  3. เลี้ยวซ้ายไปข้างหน้า : ล้อขวาไม่หมุน ล้อซ้ายหมุนไปข้างหน้า
  4. เลี้ยวขวา : ล้อซ้ายไม่หมุน ล้อขวาหมุนไปข้างหน้า
  5. ถอยไปทางซ้าย : ล้อซ้ายไม่หมุน ล้อขวาหมุนกลับหลัง
  6. ถอยไปทางขวา : ล้อขวาไม่หมุน ล้อซ้ายหมุนกลับหลัง
  7. หันหลังกลับ : ล้อซ้ายและล้อขวาหมุนไปในทิศทางตรงกันข้ามกัน

 

True table ของหุ่นหนู

เขียนเป็น True table ได้หน้าตาแบบนี้

D0 ถึง D3 คือสายข้อมูลจากพอร์ทขนาน ML Step คือสัญญาณให้สเต็ปของมอเตอร์ซ้าย ML Dir คือสัญญาณกำหนดทิศทางการหมุนของมอเตอร์ซ้าย MR Step คือสัญญาณให้สเต็ปของมอเตอร์ขวา MR Dir คือสัญญาณกำหนดทิศทางการหมุนของมอเตอร์ขวา

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

 

STEP01

ขั้นแรกก็เปิดโปรเจคใหม่ชื่อ step01 สร้างฟอร์มขึ้นมาหนึ่งฟอร์มชื่อ main บนฟอร์มใส่ปุ่มหนึ่งปุ่มชื่อ cmdStep1 แล้วใส่โค้ดดังนี้

1. Private Sub cmdStep1_Click()

2. vbOut 8880, 255 ‘ ค่าส่งไปคือ 1111 1111

3. Call Sleep(100)

4. vbOut 8880, 15 ‘ ค่าส่งไปคือ 0000 0000

5. Call Sleep(100)

6. vbOut 8880, 255 ‘ ค่าส่งไปคือ 1111 1111

7. End sub

ไม่ต้องใส่เลขบรรทัด เพราะ VB ไม่มีเลขบรรทัด ผมใส่เลขบรรทัดไว้เพื่อประโยชน์ในการอธิบาย บรรทัดที่ 2. เราส่ง 1 ออกไปทุกเส้น มอเตอร์จะไม่ทำงาน วงจรเข้าสู่สภาวะไม่แอคตีฟ (สัญญาณทุกเส้นของฟล็อปปิดิสก์จะแอคตีฟด้วยโลจิกต่ำ) บรรทัดที่ 4 และ 5 เราส่งโลจิกต่ำออกไปยังขา Step และ Direction ของวงจรขับมอเตอร์ทั้งสองตัว แล้วรอนาน 100 มิลลิวินาที จึงส่งโลจิคสูงออกไปอีกครั้ง (บรรทัดที่ 6) การทำอย่างนี้เท่ากับเราส่งพลัสเป็นสแควร์เวฟที่มีดิวตีไซเคิลขนาด 100 มิลลิวินาที ซึ่งจะทำให้วงจรขับสเต็ปปิ้งมอเตอร์พอใจ

โปรแกรม step01 นี้ถ้าเรากดปุ่ม cmdStep1 ไปเรื่อยๆ หุ่นยนต์ก็จะเคลื่อนไปข้างหน้าได้เรื่อย แต่การต้องมีคนมาคอยกดปุ่มอย่างนี้ย่อมไม่สมศักดิ์ศรีหุ่นยนต์นัก ถ้าจะให้ดีเขียนโปรแกรมควรให้เราป้อนได้ว่าจะให้ไปข้างหน้ากี่สเต็ป พอกดปุ่มแล้วมันจะบังคับหุ่นให้ไปตามที่กำหนด ดีมั๊ยครับ? ต้องดีแน่

 

STEP02

โปรเจคใหม่ชื่อ step02 สร้างฟอร์มขึ้นมาหนึ่งฟอร์มชื่อ main บนฟอร์มใส่ปุ่มหนึ่งปุ่มชื่อ cmdStep1 และใส่ text box ชื่อ text1 แล้วใส่โค้ดดังนี้

1. Private Sub cmdStep1_Click()

2. dim step_counter as integer

3. step_counter = val(text1.text)

4. for i = 1 to step_counter

5. vbOut 8880, 255 ‘ ค่าส่งไปคือ 1111 1111

6. Call Sleep(100)

7. vbOut 8880, 15 ‘ ค่าส่งไปคือ 0000 0000

8. Call Sleep(100)

9. next i

10. vbOut 8880, 255 ‘ ค่าส่งไปคือ 1111 1111

11. End sub

เขียนเสร็จแล้วทดสอบโดยวิ่งโปรแกรมแล้วพิมพ์ 20 ลงใน text box แล้วกดปุ่ม cmdStep1 โปรแกรมจะทำการหมุนมอเตอร์ไปยี่สิบสเต็ป

 

STEPBACK1

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

1. Private Sub cmdStepBack1_Click()

2. vbOut 8880, 255 ‘ ค่าส่งไปคือ 1111 1111

3. Call Sleep(100)

4. vbOut 8880, 10 ‘ ค่าส่งไปคือ 0000 1010

5. Call Sleep(100)

6. vbOut 8880, 255 ‘ ค่าส่งไปคือ 1111 1111

7. End sub

สังเกตุบรรทัดที่ 4 เราส่งค่า 0000 1010 ออกไป ทำให้สัญญาณ Direction กลับกันกับโปรแกรมที่เดินหน้า ทำให้มอเตอร์หมุนไปทิศทางตรงกันข้ามกับโปรแกรมเดินหน้า

 

STEPTEST

เมื่อเดินหน้าถอยหลังได้แล้ว ขั้นต่อไปเป็นการเขียนโปรแกรมให้เดินหน้าไป 100 สเต็ป เลี้ยวซ้าย 20 สเต็ป และเดินตรงไปอีก 100 สเต็ป

Private Sub cmdStep1_Click()

‘ เดินหน้า 100 สเต็ป

for i = 1 to step_counter

vbOut 8880, 255 ‘ ค่าส่งไปคือ 1111 1111

Call Sleep(100)

vbOut 8880, 15 ‘ ค่าส่งไปคือ 0000 0000

Call Sleep(100)

next i

‘ เลี้ยวซ้าย 20 สเต็ป

for i = 1 to step_counter

vbOut 8880, 255 ‘ ค่าส่งไปคือ 1111 1111

Call Sleep(100)

vbOut 8880, 15 ‘ ค่าส่งไปคือ 0000 0000

Call Sleep(100)

next i

vbOut 8880, 255 ‘ ค่าส่งไปคือ 1111 1111

End sub

 

รายการซื้อของ

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

1. ดิสก์ไดร์ฟที่เสียแล้วสองตัว 60 บาท

2. ล้อสองอัน 80 บาท

3. คอนเนคเตอร์ DB25 1 ตัว 30 บาท

4. ด้ามปากกาลูกลื่น 5 บาท

5. ลูกยางเทปแคสเซท 5 บาท

6. น็อต+แหวน 2 บาท

7. สายแพแบบ 10 เส้น 5 เมตร 25 บาท

8. LED สีแดงธรรมดา 8 ตัว 24 บาท

9. ตัวต้านทาน 1K ¼w 8 ตัว 2 บาท

รวมเงิน 233 บาท

 

การประกอบวงจร

เห็นวงจรแล้วคงยิ้มได้ เพราะวงจรคราวนี้ไม่ต้องซื้อหาอะไรมาบัดกรีให้วุ่นวาย ทุกอย่างแกะออกมาจากดิสก์ไดร์ฟหมด ยกเว้นคอนเนคเตอร์ DB-25 ที่ต้องจัดหามาต่างหาก

ขั้นแรกก็เอาดิสก์ไดร์ฟมาชะแหละ เมื่อเปิดออกแล้วคุณจะเห็นว่ามีแผ่นวงจรพิมพ์สองชิ้น ชิ้นแรกเป็นชิ้นที่มี DC มอตอร์ซึ่งคุณสามารถนำไปทำรถกระป๋องได้ อีกชิ้นหนึ่งเป็นชิ้นที่ต่อกับสเต็ปปิ้งมอเตอร์และมีคอนเน็คเตอร์สำหรับต่อกับสายแพฟล็อปปิดิสก์ไดร์ฟในเครื่องพีซี นี่คือแผ่นวงจรพิมพ์ที่เราจะใช้

ให้ต่อสายไฟตามวงจรในรูปนี้

1)ไฟเลี้ยง 2) 3) 4) 5)สายไฟไปพีซี 6)มอร์เตอร์ 7)ล้อ 8)ชิพควบคุมสเต็ปมอร์เตอร์

1)ไฟเลี้ยง 2)ไมโครสวิชท์ 3)สายไฟไปพีซี 4) 5)ตัวรับส่งอินฟราเรด 6)หลอด LED

ค่อยๆ แกะแผ่นวงจรพิมพ์ออก จะเห็นสายแพเส้นหนึ่งต่อไปเข้าแผ่นวงจรพิมพ์ที่มี DC มอเตอร์ ให้ตัดด้วยคัทเตอร์หรือบัดกรีออก คุณก็จะได้แผ่นวงจรพิมพ์ที่มีวงจรควบคุมสเต็ปมอเตอร์ที่สมบูรณ์ ทั้งหมดนี้คือรายละเอียดของฮาร์ดแวร์และซอฟต์แวร์ของหุ่นหนู ขอให้สนุกกับการประดิษฐ์ครับ!

วัดอุณหภูมิและความชื้นด้วย C# ตอน 1

ไปหน้าแรก | สารบัญ | Laploy.comระเบียนบทความ | บทความจากลาภลอย

 

วัดอุณหภูมิและความชื้นด้วย C# ตอน 1

เขียนโปรแกรมภาษา C# ใน .NET Framework นิยามคลาสวัดอุณหภูมิและความชื้นที่นำไปใช้ได้ทั้งใน WinForm และ WebForm

 

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

ผู้เขียนแก้ปัญหาโดยจัดทำระบบตรวจวัดอุณหภูมิและความชื้น แล้วส่งข้อมูลเหล่านี้เข้าสู่เครือข่ายอินเตอร์เน็ต ทำให้เฝ้าติดตามสถานะจากทางไกลได้ โปรแกรมส่วน client เป็น thin client คือใช้ web browser เป็นตัวเปิด web application มีโปรแกรมทั้งส่วน client และส่วน server

โปรแกรมส่วน server ใช้เทคโนโลยี ASP.NET และภาษา C# โปรแกรมส่วน client ใช้ภาษา JavaScript ทำงานประสานกับโปรแกรมภาษา C# ในฝั่ง server โดยใช้กลไก Ajax ทำให้แสดงข้อมูลได้อย่างมีพลวัต ตัวเลขและแผนภูมิถูก update อัตโนมัติทุกนาทีโดยไม่ต้องโหลดหน้าเว็บ

รูป 001 โปรแกรมส่วน client ใช้ภาษา JavaScript ทำงานประสานกับโปรแกรมภาษา C# ในฝั่ง server โดยใช้กลไก Ajax ทำให้แสดงข้อมูลได้อย่างมีพลวัต

เรื่องการทำ remote และ Ajax ผู้เขียนจะเล่าให้ฟังวันหลัง สำหรับเรื่องที่จะเล่าสูกันฟังวันนี้คือส่วนฮาร์ดแวร์และซอฟท์แวร์ที่ใช้เพื่อการตรวจวัดอุณหภูมิและความชื้น เป็นเรื่องของการเปลี่ยนข้อมูลทางกายภาพ ให้เป็นข้อมูลดิจิตอล แล้วเก็บบันทึกไว้เพื่อใช้ในการวิเคราะห์ หากท่านต้องทำโครงงานที่มีปัญหาลักษณะเดียวกัน อาจนำเทคนิคเหล่านี้ไปประยุกต์ใช้ หรืออาจเพียงแค่อ่านเล่นสนุกๆ ก็ได้

บทความนี้ท่านจะได้เรียนรู้เทคนิคการเขียนโปรแกรมในแง่มุมต่างๆ ดังนี้
• การเขียนโปรแกรมภาษา C# เพื่อรับ-ส่งข้อมูลผ่านพอร์ทอนุกรม
• การใช้งานไลบรารีส่วนพอร์ทอนุกรมของ .NET Framework
• วิธีนิยามคลาสหุ้มห่อบอร์ดอ่านอุณหภูมิและความชื้น
• การเชื่อมต่อคอมพิวเตอร์กับบอร์ดอ่านอุณหภูมิและความชื้นผ่าน RS-485
• ตัวอย่างการประยุกต์ใช้งาน Delegate และ event เพื่อการทำ call back ระหว่าง object
• แสดงตัวอย่างการใช้งาน anonymous method
• ตัวอย่างการประยุกต์ใช้งาน Delegate และ Invoke เพื่อการทำ call back ระหว่าง thread

ผู้เขียนส่งบทความนี้ให้ท่านบก. คือคุณสุวัจชัยพิจารณา บก. แจ้งว่า เนื้อหายาวเกินกว่าจะลงพิมพ์จนจบในฉบับเดียวได้ ให้ไปแยกมาเป็นสองตอน ที่ท่านกำลังอ่านอยู่นี้คือตอนแรก ผู้เขียนจะพูดถึง อุปกรณ์ คือส่วนฮาร์ดแวร์ที่เราจำเป็นต้องใช้ในโครงงานนี้ เป็นฮาร์ดแวร์ที่หาซื้อสำเร็จรูปได้ ไม่จำเป็นต้องสร้างขึ้นเอง วิธีนำอุปกรณ์มาต่อเข้าด้วยกันเป็นระบบ วิธีเขียนคำสั่งควบคุมอุปกรณ์ และวิธีที่อุปกรณ์กับคอมพิวเตอร์สื่อสารกัน จากนั้นจะพูดถึงวิธีนิยามคลาสในภาษา C# เพื่อห่อหุ้มอุปกรณ์และพอร์ทอนุกรมไว้เป็นก้อนเดียวกัน

อุปกรณ์ที่ต้องใช้

ระบบนี้มีอุปกรณ์หลักๆ สี่ชิ้น คือเครื่องคอมพิวเตอร์ บอร์ดอ่านอุณหภูมิและความชื้น (ต่อไปจะเรียกย่อว่า AP170) เครื่องแปลงสัญญาณแบบ RS-232 ไปเป็น RS-485 และหัววัดอุณหภูมิและความชื้น ดังแสดงในแผนภูมินี้

รูป 002 แผนภูมิแสดงอุปกรณ์และการเชื่อมต่อสำหรับระบบอ่านอุณหภูมิและความชื้นกับคอมพิวเตอร์


รูป 003 ตัวแปลง RS-232/RS485

 
รูป 004 บอร์ดอ่านอุณหภูมิและความชื้น

 
รูป 005 หัววัดอุณหภูมิและความชื้น

รูป 006 เมื่อนำทั้งหมดมาต่อเชื่อมเข้าด้วยกัน

อุปกรณ์ทั้งหมดผลิตขึ้นในประเทศไทย ผู้เขียนซื้อมาจากพระโขนง หัววัดเป็นแบบวัดอุณหภูมิและความชื้นได้ในตัวเดียวกัน วัดอุณหภูมิความละเอียดขั้นละ .1 องศา ให้ค่าออกมาเป็นตัวเลของศาเซลเซียส ส่วนการวัดความชื้น วัดได้ที่ความละเอียดขั้นละ 1% ให้ตัวเลขออกมาสัดส่วนร้อยละของความชื้นสัมพัทธ์

บอร์ด AP170 อันที่จริงแล้วก็คือคอมพิวเตอร์ตัวหนึ่ง เราสามารถนำ AP170 หลายๆ บอร์ดมาต่อเชื่อมโยงเป็นเครือข่ายเหมือนระบบ LAN ได้ วิธีเชื่อมโยงใช้มาตรฐานแบบ RS-485 เมื่อเราจะนำมาต่อกับพอร์ทอนุกรมซึ่งเป็นมาตรฐาน RS-232 จึงต้องแปลงสัญญาณเสียก่อน โดยใช้ตัวแปลงสัญญาณอย่างที่เห็นในรูป

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

กล่องดำ

ปรกติคนจะนำ AP170 ไปใช้กับไมโครคอนโทรลเลอร์ แล้วเขียนโปรแกรมควบคุมด้วยภาษา แอสแซมบลี หรือภาษา ซี แต่งานนี้เราจะนำมันมาใช้กับพีซี โดยพัฒนาเป็นโปรแกรมประยุกต์ใช้งานใน Windows ด้วยภาษา C# และ .NET Framework โปรแกรมนี้จึงทำงานได้ใน Windows 98/ME/XP และ Vista

ผู้เขียนจะใช้หลักการ encapsulation และ information hiding เพื่อนิยามคลาสห่อหุ้มพอร์ทสื่อสารและบอร์ด AP170 เข้าเป็นก้อนเดียวกัน เพื่อให้การนำไปใช้งานทำได้ง่าย ท่านสามารถนำคลาสนี้ไปสร้างเป็น object ที่ทำงานภายในโครงงานต่างๆ ได้ โดยไม่ต้องพะวงกับรายละเอียดปลีกย่อยทางฮาร์ดแวร์ เหมือนเราได้กล่องดำที่ไม่จำเป็นต้องรู้รายละเอียดของการทำงานภายในก็สามารถนำไปใช้งานได้

การรับ-ส่งข้อมูลกับบอร์ดวัดอุณหภูมิ

AP170 มีฟังก์ชันไม่ซับซ้อน โปรโตคอลในการรับ-ส่งข้อมูลจึงเรียบง่าย การส่งคำสั่งจากพีซีไปยังบอร์ดใช้วิธีส่งเป็นตัวอักษร (เป็นสายอักขระหรือ string) เหมือนคำสั่งควบคุมโมเด็ม ยกตัวอย่างเช่น คำสั่งอ่านอุณหภูมิและความชื้นคือ “:1<cr>” เราเพียงแค่ส่งตัวอักษร : และ 1 และรหัสปุ่ม Enter ซึ่งในภาษา C# จะใช้ “\r”

เมื่อส่งคำสั่งไปแล้ว โปรแกรมของเราจะต้องรอรับคำตอบ เพราะ AP170 จะส่งข้อมูลกลับมาภายในครึ่งวินาที โดยเป็นข้อมูลตัวอักษรที่มีรูปแบบแน่นอนดังนี้ Txxx.x Hxxx.x<cr> ยกตัวอย่างเช่นหากอุณหภูมิเป็นสามสิบหาจุดห้าความชื้นร้อยละห้าสิบจุดสอง ข้อมูลที่ AP170 ส่งมาจะเป็น T036.5 H50.2<cr>

คลาสหุ้มห่อบอร์ดวัดอุณหภูมิและความชื้น

คลาสที่จะนิยามนี้ชื่อ TempReader ชื่อคลาสนี้มากจากคำสองคำ คือ temperature และ Reader ถ้าท่านไม่ชอบจะเปลี่ยนชื่อเป็นอย่างอื่นก็ได้ คลาสนี้เป็นคลาสที่เบ็ดเสร็จในตัว คือไม่ได้อ้างถึงตัวแปรหรือตัวคงค่า หรือ method ของคลาสอื่นๆ ใน project เดียวกันนี้

คลาส TempReader เป็นคลาสที่ถูกออกแบบมาให้นำไปใช้สร้าง object ไม่ใช่คลาสสำหรับนำไปทำเป็น base class แต่ถ้าท่านต้องการจะนำไปใช้เป็น base class เพื่อพัฒนาต่อยอด ก็สามารถทำได้เช่นกัน

รูป 007คลาสไดอะแกรมของ TempReader จะเห็นว่าคลาสนี้มีสมาชิกหลายแบบ คือมีฟิลด์สมาชิกสามตัว property สมาชิกหนึ่งตัว method สมาชิกสี่ตัว และสมาชิกแบบ event หนึ่งตัว โปรดสังเกตว่า delegate มีภาวะเป็น nested type

โค้ดของคลาส TempReader ค่อนข้างสั้น เพราะเจตนาเขียนให้อ่านง่าย ไม่ซับซ้อน ซอร์สโค้ด เป็นดังที่เห็นข้างล่าง หากท่านไม่ต้องการป้อนพิมพ์ก็สามารถดาวน์โหลดมาทดสอบการทำงานได้จากเว็บไซต์ของผู้เขียน ( http://www.laploy.com/download ไฟล์ชื่อ TempReader.cs)

using System;
using System.Collections.Generic;
using System.Text;
using System.IO.Ports;
using System.Threading;

namespace Climsel
{
    class TemppReader
    {
        public delegate void TemppReaderEventHandler(string tempp);
        public event TemppReaderEventHandler TemppReaderFire;
        private SerialPort myTemppComPort = new SerialPort();
        private int temppComPort;
        private string rxString;

        public int TemppComPort
        {
            get { return temppComPort; }
            set { temppComPort = value; }
        }

        public void GetTempp(string code)
        {
            if (!myTemppComPort.IsOpen) myTemppComPort.Open();
            myTemppComPort.Write(code);
        }
        public void CloseCom()
        {
            myTemppComPort.Close();
        }
        public void CreateTemppConnection()
        {
            myTemppComPort.Close();
            myTemppComPort.BaudRate = 9600;
            myTemppComPort.PortName = "COM" + temppComPort.ToString();
            myTemppComPort.Parity = Parity.None;
            myTemppComPort.DataBits = 8;
            myTemppComPort.StopBits = StopBits.One;
            myTemppComPort.RtsEnable = true;

            myTemppComPort.DataReceived += new SerialDataReceivedEventHandler(myTemppComPort_DataReceived);
        }
        private void myTemppComPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            rxString += myTemppComPort.ReadExisting();
            if (rxString[rxString.Length-1] == '\r' )
            {
                TemppReaderFire(rxString);
                rxString = "";
            }
        }
    }
}

โค้ดส่วนบนสุดคือกลุ่มคำสั่ง using ทำหน้าที่จับรวม namespace ห้า namespace เข้ามาคอมไพล์ร่วมด้วย คำสั่ง using เป็นคำสั่งในภาษา C# ทำหน้าที่คล้ายๆ คำสั่ง include ในภาษาซี หรือภาษา PHP แตกต่างกันตรงที่คำสั่ง include ทำหน้าที่ผนวกฟังค์ชันจากไฟล์ที่เป็นซอร์สโค้ด ขณะที่คำสั่ง using ทำหน้าที่ผนวก type ภายใน namespace ที่อ้างถึง โดย type เหล่านั้นอยู่ในสภาพไฟล์แอสเซมบลี (คือไฟล์นามสกุล .dll ของ .NET Framework) ที่คอมไพล์ไว้แล้ว

สำหรับผู้ที่ใช้ภาษาจาวาและ VB.NET โปรดเข้าใจว่าคำสั่ง using ก็คือคำสั่ง import นั่นเอง คำสั่ง using ทั้งหมดถูกแทรกเข้ามาโดยอัตโนมัติโดยโปรแกรม Visual Studio .NET ยกเว้น using System.IO.Ports;
ที่ผู้เขียนใส่เข้าไปเอง เพราะ namespace นี้เกี่ยวข้องโดยตรงกับการใช้งานพอร์ทอนุกรม

ถัดมาคำสั่ง namespace Laploy.TempHumReader ทำหน้าที่ประกาศ namespace ที่ผู้เขียนกำหนดขึ้นสำหรับโปรแกรมนี้ .NET Framework สนับสนุนให้เราตั้ง namespace ขึ้นเองเพื่อป้องกัน error ที่เกิดจากการตั้งชื่อ type ซ้ำกัน ซึ่งมีโอกาสเกิดขึ้นได้หากทำงานเป็นทีม หรือมีการนำ type ที่เคยนิยามไว้กลับมาใช้ใหม่

ต่อมาคำสั่ง class TemppReader ทำหน้าที่บอกจุดเริ่มต้นของนิยามคลาส โปรดสังเกตว่าผู้เขียนใช้วิธีตั้งชื่อคลาสแบบ ปาสคาล (pascal case) ซึ่งเป็นธรรมเนียมปฏิบัติของการตั้งชื่อคลาสในภาษา C#

การประกาศ Delegate และ Event

สองบรรทัดถัดมาคือการประกาศ delegate และ event โดยกำหนดให้ทั้งสองตัวมี access modifier เป็นแบบ public ทั้งคู่ เพราะเราต้องการให้คลาสที่จะใช้งาน object นี้ (คือโปรแกรมที่เรียกใช้งานคลาสนี้ ต่อไปจะเรียกว่า client class) สามารถเข้าถึงมันได้

ในคลาสนี้ผู้เขียนจะสาทิตการใช้ delegate เพื่อทำงานแบบ call back เพื่อให้ client class รับรู้การทำงานเมื่อพอร์ทอนุกรมได้รับข้อมูลจาก AP170 สำหรับนักเขียนภาษาซี และ C++ ขอให้เข้าใจว่านี่เป็นการทำงานเลียนแบบ function pointer แตกต่างจาก function pointer ตรงที่เป็นกลไกซึ่งมีคุณสมบัติ type safety โดยสมบูรณ์ (ไม่ต้องใช้ pointer)

คำสั่ง public delegate void TemppReaderEventHandler(string tempp); ทำหน้าที่ประกาศ delegate ชื่อ TemppReaderEventHandler โปรดสังเกตว่ารูปแบบการประกาศ delegate คล้ายการนิยามส่วนหัวของ method เพราะมีทั้ง return type, ชื่อ method และพารามิเตอร์ เราเรียกการประกาศเช่นนี้ว่า anonymous method เพราะเป็น method ที่เราไม่ได้นิยามไว้จริง รูปแบบเช่นนี้ช่วยอำนวยความสะดวกในการใช้งาน delegate (anonymous method เริ่มมีใช้ใน .NET 2.0)

บรรทัดถัดมา public event TemppReaderEventHandler TemppReaderFire; ทำหน้าที่ประกาศ event ที่เราต้องการให้ทำงานเมื่อพอร์ทอนุกรมได้รับข้อมูลจาก AP170 เมื่อเกิด event ขึ้น delegate จะทำงานโดยเรียก anonymous method ให้ทำงาน ถ้าอ่านถึงตรงนี้แล้วยังไม่เข้าใจก็ไม่เป็นไร เพระอีกสักครู่ เมื่อท่านเห็นส่วนใช้งาน anonymous method แล้วจะเข้าใจดีขึ้น

โปรดสังเกตว่าการประกาศ delegate เป็นการนิยาม type ใหม่ ในกรณีนี้เป็น type ที่ซ้อนอยู่ภายในคลาส TemppReader การประกาศเช่นนี้เรียกว่า nested type (ดูในคลาสไดอะแกรม)

การประกาศฟิลด์สมาชิก

ฟิลด์สมาชิกทำหน้าที่เก็บข้อมูลใน object ทำให้ object มีความเบ็ดเสร็จในตัวตามหลักการ encapsulation ในกรณีที่ไม่ใช่ static class (ตามในตัวอย่างนี้) object แต่ละตัวจะมีฟิลด์สมาชิกเป็นของตัวเอง ยกตัวอย่างเช่น หากเราสร้าง object สาม object จะเกิดฟิลด์สมาชิกสามชุด สำหรับแต่ละ object ไม่ปะปนกัน

คลาส TempReader มีการประกาศสมาชิกแบบฟิลด์สามตัว มีภาวะเป็น private ทั้งสามตัว สมาชิกแบบฟิลด์ที่เป็น private จะมีขอบเขตอยู่ภายใน object ที่จะถูกสร้างจากคลาสนี้เท่านั้น ไม่สามารถถูกอ่าน-เขียนจาก client class ได้ จึงมักถูกเรียกว่า instance filed หรือ object filed

ฟิลด์ชื่อ myTemppComPort เป็นตัวแปรแบบ reference type ทำหน้าที่ใช้อ้างอิง object ซึ่งมี type เป็น SerialPort อันเป็น type ที่เกิดจากคลาสชื่อเดียวกันใน namespace ชื่อ System.IO.Ports ที่เราจับรวมไว้ด้วยคำสั่ง using ดังที่อธิบายไปตอนต้น

เมื่อสร้าง object ซึ่งทำหน้าที่ควบคุมพอร์ทอนุกรมแล้ว การใช้งานพอร์ทอนุกรมต่อไปจะทำได้ง่ายมาก เพราะเราเพียงกำหนด property ต่างๆ ให้แก่ฟิลด์ myTemppComPort และเรียก method ต่างๆ ของมัน เพียงเท่านี้เราก็จะสามารถรับ-ส่งข้อมูลกับพอร์ทอนุกรมได้แล้ว ท่านจะได้เห็นตัวอย่างวิธีเรียกใช้ object นี้ในโค้ดส่วนทดสอบการใช้งานตอนท้ายของบทความ (คลาส test)

ถัดมาคือฟิลด์ชื่อ temppComPort ทำหน้าที่เก็บค่าหมายเลขพอร์ท ในเครื่องพีซีโดยทั่วไปจะมีพอร์ทอนุกรมสองพอร์ท ชื่อ COM1 และ COM2 เดิมที่เรามักใช้พอร์ท COM1 เพื่อต่อกับเมาส์ แต่ปัจจุบันเมาส์ส่วนใหญ่เป็นอินเตอร์เฟสแบบ USB หมดแล้ว พอร์ท COM1 และ COM2 จึงไม่ได้ใช้และถูกปล่อยทิ้งว่างไว้

ฟิลด์ตัวสุดท้ายชื่อ rxString ทำหน้าที่รับข้อมูลอุณหภูมิและความชื้นจาก AP170

โปรดติดตามตอนจบ

นิยามคลาส TempReader ยังไม่จบ แต่หน้ากระดาษหมดเสียก่อน ที่ท่านอ่านไปแล้วในตอนนี้เป็นเรื่องอุปกรณ์ที่เราต้องใช้เพื่อรับข้อมูลอุณหภูมิและความชื้น วิธีนำอุปกรณ์เหล่านี้มาต่อเชื่อมกันเป็นระบบ วิธีเขียนคำสั่งควบคุมอุปกรณ์ วิธีที่อุปกรณ์และคอมพิวเตอร์สื่อสารกัน และวิธีนิยามคลาส TempReader เพื่อห่อหุ้มอุปกรณ์และพอร์ทอนุกรมไว้เป็นก้อนเดียวกัน

ในตอนหน้าซึ่งเป็นตอนจบ เราจะมาดูนิยามส่วนที่เหลือของ คลาส TempReader ได้แก่ส่วนประกาศสมาชิกแบบ property ส่วนนิยาม method สมาชิก และดูตัวอย่างวิธีสร้าง object จากคลาส TempReader ซึ่งจะช่วยให้ท่านจะสามารถนำ object นี้ไปใช้งานในโครงงานต่างๆ ของท่านได้ทันที