Неизменяемые данные. Immutable Object

Последнее обновление: 10.09.2023

Иногда необходимо предоставить доступ к данным извне таким образом, чтобы они не могли быть изменены и были доступны только для чтения.

Стандартным решением в этом случае будет определение данных в статической памяти. Данные могут определяться как на этапе компиляции, так и на этапе запуска приложения, и после этого они не изменяются. А для доступа к данным определяется функция, которая возвращает указатель на эти данные. Причем возвращается указатель на константу, чтобы данные не могли быть изменены извне. В общем случае это выглядит следующим образом. На одной стороне - стороне источника данных определяются собственно данные и функция для доступа к ним:

// структура, которая представляет неизменяемые данные
struct Immutable
{
  int x;
  int y;
};

// данные, которые храняться в статической памяти
static struct Immutable data = {11, 22};

// функция, которая возвращает ссылку на данные
const struct Immutable* getData()
{
   return &data;
}

На другой стороне - стороне приемника данных вызываем выше определенную функцию и получаем данные в указатель на константу:

const struct Immutable* immut_data;     // чтобы данные не изменились, определяем указатель на константу
immut_data = getData();         // получем данные из источника данных

Подобный паттерн иногда называют Immutable Object или Immutable Instance. Рассмотрим простейший вариант применения. Пусть у нас есть файл user.h, где определяется структура используемых данных:

struct User{
  char* name;
  unsigned age;
  char* email;
};

Структура User имеет три поля. Это те данные, которые будут представлять неизменяемые данные.

Определим файл user.c, который определяет неизменяемые данные и API для доступа к ним:

#include "user.h"

static struct User data = {"Tom", 39, "tom@smail.ru"};
const struct User* getUser(void)
{
    return &data;
}

Здесь данные представляют объект data, который представляет структуру User и который размещен в статической памяти. Обратите внимание, что это не константный объект. Он может быть установлен и в процессе работе программы и в принципе даже может быть изменен. Однако он определен с помощью ключевого слова static, поэтому напрямую (без применения указателей) из вне файла user.c не виден.

Для доступа к этому объекту определена функция getUser, которая возвращает указатель на константу - адрес объекта data.

И также определим основной файл приложения app.c, в котором получаем этот объект:

#include <stdio.h>
#include "user.h"

extern const struct User* getUser(void);

int main(void)
{
  const struct User* user;  // получаемый объект представляет указатель на константу
  user = getUser(); // получаем объект в константу

  printf("name=%s\n", user->name);
  printf("email=%s\n", user->email);
  printf("age=%d\n", user->age);
}

Здесь вызываем функцию getUser из файла "user.c" и получаем данные в указатель на константу.

Скомпилируем оба файла в приложение и запустим его:

c:\C>gcc -Wall -pedantic app.c user.c -o app

c:\C>app
name=Tom
emai=tom@smail.ru
age=39

c:\C>

Таким образом, компонент-потребитель данных (код в файле app.c) не должен заботиться об управлении памяти, посольку данные определены в статической памяти. Данный подход может успешно использоваться в многопоточных приложениях, поскольку потоки не изменяют данные, а только читают, и для всех обращающих потоков данные остаются теми же.

Однако здесь также есть недостатки. Например, что если код в файле app.c будет получать указатель на неконстантый объект:

struct User* user = getUser();
user->name = "Bob";

Хотя при компиляции компиляторы могут выдать предупреждение, но код скомпилируется, нормально отработает, а значения по полученному указателю изменятся.

Кроме того, данные могут измениться на стороне компонента user.c. Соответственно в данном случае мы имеем дело с относительной неизменяемостью данных.

Стоит отметить, что мы могли бы пойти дальше в сторону инкапсуляции и определить объект в user.c в самой возвращаеющей его функции:

#include <stdio.h>
#include "user.h"

const struct User* getUser(void)
{
    static struct User data = {"Tom", 39, "tom@smail.ru"};
    return &data;
}

void printUser(void){
    const struct User* user;
    user = getUser();
    printf("name=%s\n", user->name);
    printf("emai=%s\n", user->email);
    printf("age=%d\n", user->age);
}

И также внутри "user.c" для получения объекта вызываем функцию getUser и получаем указатель на константу.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850