Перейти к содержимому

Что такое контекст в c

  • автор:

Что такое контекст? Более обширный взгляд

Стандартные библиотеки языка очень образные и что бы понять семантику контекста надо прежде всего обратиться к самому понятию «контекст» в общепринятом понимании, а не в специфике программирования: Конте́кст (от лат. contextus — «соединение», «связь») — законченный отрывок, общий смысл которого позволяет уточнить значение входящих в него отдельных слов, предложений, и т. п. _ _Говорить, опираясь на контекст, — значит придерживаться установившегося в разговоре уровня абстракции и использовать понятия заданного в нём семантического поля.

23 мар 2017 в 11:19

В более широком значении контекст — среда, в которой существует объект. С формальной точки зрения контекст представляет собой определённую систему отсчета, пространство имён. Любое событие, происходящее в жизни субъекта, интерпретируется исходя из контекста ситуации, отражённой в памяти субъекта.

23 мар 2017 в 11:19

В программировании понятие контекст имеет те же свойства и семантическую нагрузку — давать элементу переменные окружения, обуславливающие его работу

23 мар 2017 в 11:21
(: А по вопросу мне нечего ответить
23 мар 2017 в 11:22
@Tachkin, отличный ответ 🙂 Я плюсану, оформляйте. 🙂
23 мар 2017 в 11:40

2 ответа 2

Сортировка: Сброс на вариант по умолчанию

класс Context содержит в себе всевозможную информацию о ресурсах системы, как уже было сказано в другом ответе. Конкретно в этом вопросе нас интересует, что он содержит, помимо прочего, и параметры темы (стилей) для отображения View

Почему в качестве контекста можно передать this? Это же ссылка на класс

Активити является наследником класса Context и несет в себе информацию о контексте для этой активити, поэтому мы можем использовать ссылку на именно этот класс в качестве контекста. С Fragment , к примеру, это уже не работает — он не наследуется от Context

Есть ли случаи когда надо передать именно getApplicationContext?

Тема (стиль) всего приложения и конкретной активити может отличаться (для активити в манифесте указан другой стиль). Тогда запрос контекста приложения и контекста активти вернет разное оформление View

Почему контекст нужен везде, где происходит работа с интерфейсом?

Потому что он содержит стиль для View

на остальные вопросы ответ тот же — в контексте содержится информация, как должен выглядеть View . Например, стиль кнопки темы Holo и темы AppCompat сильно отличается, в контексте и содержится эта информация.

Возможно в приложении могут существовать и какие то другие отличия в окружении, назначенном всему приложению и конкретной активити, тогда обращение к контексту приложения или активити тоже будет иметь значение, но мне такие отличия (кроме тем и стилей) припомнить не удалось.

UPD несколько важных замечаний по getApplicationContext() , не связанных с UI приложения из этой статьи

  1. контекст приложения следует использовать везде, где контекст необходимо передать за пределы жизненного цикла передающего компонента (в объекты, которые будут жить дольше, чем создавшая/вызвавшая их активность, например) во избежании удержания ссылки на этот компонент при использовании его собственного контекста и утечек памяти.
  2. во внешние библиотеки следует передавать контекст приложения по тем же причинам, что и п.1
  3. контекст приложения не имеет информации по особенностям GUI отдельной активити, если они отличаются от параметров всего приложения, в таких случаях нельзя использовать контекст приложения при работе с GUI этой активити.
  4. приложение (класс Application ) — синглтон и его контекст тоже синглтон, этот контекст может удерживать объекты с более коротким жизненным циклом и приводить к утечкам памяти, если не позаботиться о их корректной обработке GC

Разбираемся с контекстами в Go

Этот пост является конспектом моего видео про контексты:

Я заметил, что тема контекстов в языке Go у многих почему-то вызывает сложности с пониманием. Возможно, это связано с тем, что контекст — это очень абстрактная сущность и не встречается в других языках программирования в таком виде, по крайней мере в тех языках, что довелось использовать мне.

В общем, я решил написать материал по данной теме с примерами и лучшими практиками. Вам будет легче понять то, о чем я буду рассказывать, если вы уже знакомы с основами языка Go, в частности знаете, что такое горутины и каналы.

Что такое контекст?#

Если мы взглянем на документацию к пакету context , то первый абзац будет таким:

Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.

Пакет context определяет тип Context, который позволяет управлять дедлайнами, сигналами отмены и другими значениями области действия запросов между границами API и процессами.

Что это, чёрт побери, значит?

А значит это примерно следующее:

Контекст — это объект, который предназначен в первую очередь для того, чтобы иметь возможность отменить извне выполнение потенциально долгой операции. Кроме того, с помощью контекста можно хранить и передавать информацию между функциями и методами внутри вашей программы.

Отменять долгие операции с помощью контекста можно несколькими способами:

  • По явному сигналу отмены ( context.WithCancel )
  • По истечению промежутка времени ( context.WithTimeout )
  • По наступлению временной отметки или дедлайна ( context.WithDeadline )

Пример. Столик в ресторане#

Вы хотите забронировать столик в ресторане. Для этого вы набираете ресторан, и ждете, пока на той стороне возьмут трубку. Далее происходит одно из двух:

  1. Сотрудник ресторана берёт трубку. В таком случае вы начинаете диалог — всё хорошо;
  2. На той стороне никто не берёт трубку в течение минуты, двух, трёх…

Во втором случае вы не будете ждать вечно, и положите трубку, когда вам надоест ждать, либо вы поймёте, что это не имеет смысла.

Похожим образом работает контекст с таймаутом: как только проходит определённый промежуток времени, исполнение операции останавливается.

context.WithTimeout() #

Ваше приложение отправляет запрос во внешнюю систему, например, в API другого сервиса, который владеет интересующими вас данными. Так как мы не контролируем внешние системы, мы не можем быть на 100% уверены, что API ответит за приемлемое время, или вообще ответит когда-либо. Чтобы не зависнуть навечно в ожидании ответа от API, в запрос можно передать контекст:

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 
ctx, cancel := context.WithTimeout(context.Background(), 15 * time.Second) defer cancel()  req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com", nil) if err != nil   return nil, fmt.Errorf("failed to create request with ctx: %w", err) >  res, err := http.DefaultClient.Do(req) if err != nil   return nil, fmt.Errorf("failed to perform http request: %w", err) >  return res, nil 

Давайте разбираться, что здесь написано.

ctx, cancel := context.WithTimeout(context.Background(), 15 * time.Second) defer cancel() 

Первым делом происходит инициализация контекста с таймаутом в 15 секунд. Конструкция defer cancel() гарантирует, что после выхода из функции или горутины контекст будёт отменён, и таким образом вы избежите утекания горутины — явления, когда горутина продолжает выполняться и существовать в памяти, но результат её работы больше никого не интересует.

В следующем блоке ничего особенного, создаётся объект *http.Request , куда встраивается созданный нами контекст:

1 2 3 4 
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com", nil) if err != nil   return nil, fmt.Errorf("failed to create request with ctx: %w", err) > 

Ну, и непосредственно исполнение запроса:

1 2 3 4 
res, err := http.DefaultClient.Do(req) if err != nil   return nil, fmt.Errorf("failed to perform http request: %w", err) > 

Дерево контекстов#

Вы можете спросить: а что за context.Background() ?

Дело в том, что любой контекст должен наследоваться от какого-то другого, родительского контекста. Исключения: Background и TODO . Background — это контекст-заглушка, используемый как правило как самый верхний родитель для всех дочерних контекстов в иерархии. TODO — это тоже заглушка, но используется в тех случаях, когда мы ещё не определились, какой тип контекста мы хотим использовать. Эти два типа контекста по сути одно и тоже, и разница исключительно семантическая.

Окей, зачем нужна схема с родительскими и дочерними контекстами? Это сделано для того, чтобы внутри функции, куда был проброшен контекст, не было возможности повлиять на условия отмены сверху. Таким образом мы имеем гарантию (с некоторым оговорками), что контекст с дедлайном отменится не позже данного дедлайна. Кроме того, это даёт возможность дополнять родительский контекст и передавать дальше по цепочке новый контекст, обогащённый новыми данными.

Для наглядности рассмотрим ещё один пример. Если мы запустим программу ниже, мы увидим: несмотря на то, что внутри функции doWork таймаут переопределяется на больший, отмена контекста все равно наступит через 10 секунд:

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 
package main  import (  "context"  "log"  "time" )  func main()   ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)  defer cancel()   doWork(ctx) >  func doWork(ctx context.Context)   newCtx, cancel := context.WithTimeout(ctx, 30*time.Second)  defer cancel()   log.Println("starting working. ")   for   select   case Done():  log.Printf("ctx done: %v", ctx.Err())  return  default:  log.Println("working. ")  time.Sleep(1 * time.Second)  >  > > 

context.WithDeadline() #

Контекст с таймаутом по сути является удобной обёрткой над контекстом с дедлайном. Программу из предыдущего примера можно выразить немного по-другому:

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 
package main  import (  "context"  "log"  "time" )  func main()   ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(10*time.Second))  defer cancel()   doWork(ctx) >  func doWork(ctx context.Context)   newCtx, cancel := context.WithTimeout(ctx, 30*time.Second)  defer cancel()   log.Println("starting working. ")   for   select   case Done():  log.Printf("ctx done: %v", ctx.Err())  return  default:  log.Println("working. ")  time.Sleep(1 * time.Second)  >  > > 

time.Now().Add(10*time.Second) — это ровно то, что делает функция context.WithTimeout() , вызывая внутри себя context.WithDeadline() .

context.WithCancel() #

Представьте, что вы очень торопитесь на важную встречу. Чтобы добраться побыстрее, вы решаете вызвать такси. Где быстрее найдётся машина, вы не знаете, поэтому решаете начать поиск в нескольких сервисах одновременно. Когда в одном из них найдётся машина, вы отмените поиск в остальных. Для похожих и других задач можно использовать контекст с функцией отмены.

Давайте представим, как описанная ситуация могла бы выглядеть в виде кода:

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 
package main  import (  "context"  "log"  "math/rand"  "sync"  "time" )  func main()   var (  resultCh = make(chan string)  ctx, cancel = context.WithCancel(context.Background())  services = []string"Super", "Villagemobil", "Sett Taxi", "Index Go">  wg sync.WaitGroup  winner string  )   defer cancel()   for i := range services   svc := services[i]   wg.Add(1)  go func()   requestRide(ctx, svc, resultCh)  wg.Done()  >()  >   go func()   winner =   cancel()  >()   wg.Wait()  log.Printf("found car in %q", winner) >  func requestRide(ctx context.Context, serviceName string, resultCh chan string)   time.Sleep(3 * time.Second)   for   select   case Done():  log.Printf("stopped the search in %q (%v)", serviceName, ctx.Err())  return  default:  if rand.Float64() > 0.75   resultCh   return  >   continue  >  > > 

context.WithValue() #

Окей, а что насчёт передачи значений через контекст? Для этого в пакете существует функция WithValue . Давайте взглянем, как это работает:

 1 2 3 4 5 6 7 8 9 10 11 12 13 
package main  import (  "context"  "log" )  func main()   ctx := context.WithValue(context.Background(), "name", "Joe")   log.Printf("name = %v", ctx.Value("name"))  log.Printf("age = %v", ctx.Value("age")) > 

Обратите внимание, что метод Value возвращает значение типа interface<> , поэтому скорее всего вам будет необходимо привести его к нужному типу. Кроме того, если ключ не представлен в контексте, метод вернёт nil .

Когда стоит передавать данные через контекст?#

Короткий ответ — никогда. Передача данных через контекст является антипаттерном, поскольку это порождает неявный контракт между компонентами вашего приложения, к тому же ещё и ненадёжный. Исключение составляют случаи, когда вам нужно предоставить компоненту из внешней библиотеку вашу реализацию интерфейса, который вы не можете менять. Например, middleware в HTTP сервере.

Пример. HTTP Middleware#

Представьте, что вы хотите, чтобы ваш API принимал запросы только от аутентифицированных клиентов. Однако вызывать методы для аутентификации в каждом обработчике не кажется удачной идеей. Но вы можете сделать так, чтобы перед тем как вызовется обработчик запроса, вызвался метод, который проведёт аутентификацию, и либо вызовет следующий метод в цепочке (в данном случае обработчик), либо вернёт HTTP с ошибкой аутентификации. Это и есть пример классического middleware.

Вот как это может выглядеть:

 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 
package main  import (  "context"  "fmt"  "io"  "log"  "net/http" )  type ctxKey string  const keyUserID ctxKey = "user_id"  func main()   mux := http.NewServeMux()   mux.Handle("/restricted", authMiddleware(handleRestricted()))   if err := http.ListenAndServe(":3000", mux); err != nil   log.Fatal(err)  > >  func authMiddleware(next http.Handler) http.Handler   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request)   token := r.Header.Get("Token")   if token != "very-secret-token"   w.WriteHeader(http.StatusUnauthorized)  io.WriteString(w, "invalid token")  return  >   ctx := context.WithValue(r.Context(), keyUserID, 42)  next.ServeHTTP(w, r.WithContext(ctx))  >) >  func handleRestricted() http.Handler   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request)   userID, ok := r.Context().Value(keyUserID).(int)  if !ok   w.WriteHeader(http.StatusInternalServerError)  io.WriteString(w, "internal error, try again later please")  return  >   io.WriteString(w, fmt.Sprintf("hello, user #%d!", userID))  >) > 

Когда использовать контекст?#

  • Метод ходит куда-то по сети;
  • Горутина исполняется потенциально «долго».

Если у вас есть сомнения насчёт того, соответствует ли функция одному из этих критериев, то лучше всё-таки добавить контекст. Это не усложнит вам жизнь, но потенциально упростит её в будущем. Особенно это касается объявляемых вами интерфейсов — внутри реализации может происходить всё что угодно, в том числе сетевые вызовы и долгие операции.

Советы и лучшие практики#

  • Передавайте контекст всегда первым аргументом — это общепринятое соглашение;
  • Передавайте контекст только в функции и методы, не храните в состоянии (внутри структуры). Контексты спроектированы так, чтобы их использовали как одноразовые и неизменяемые объекты. Например, если вы сохраните контекст с таймутом в 15 секунд в поле структуры, а спустя 15 секунд попробуете выполнить операцию с данным контекстом, у вас ничего не получится. Обнулить счётчик таймаута вы тоже не сможете;
  • Используйте context.WithValue только в крайних случаях. В 99,(9)% случаев вы сможете передать данные через аргументы функции;
  • context.Background должен использоваться только как самый верхний родительский контекст, поскольку он является заглушкой и не предоставляет средств контроля;
  • Используйте context.TODO , если пока не уверены, какой контекст нужно использовать;
  • Не забывайте вызывать функцию отмены контекста, т.к. функции, принимающей контекст может потребоваться время на завершение перед выходом;
  • Передавайте только контекст, без функции отмены. Контроль за завершением контекста должен оставаться на вызывающей стороне, иначе логика приложения может стать очень запутанной.

Полезные ссылки#

  • pkg.go.dev / Документация к пакету context
  • The Go Blog / Больше примеров от авторов языка
  • Go by Example / Раздел о контекстах

Почему контекст не является «инструментом для управления состоянием»

Нет. Это разные инструменты, делающие разные вещи и используемые в разных целях.

Является ли контекст инструментом «управления состоянием»?

Нет. Контекст — это форма внедрения зависимостей (dependency injection). Это транспортный механизм, который ничем не управляет. Любое «управление состоянием» осуществляется вручную, как правило, с помощью хуков useState()/useReducer().

Являются ли Context и useReducer() заменой Redux?

Нет. Они в чем-то похожи и частично пересекаются, но сильно отличаются в плане возможностей.

Когда следует использовать контекст?

Когда вы хотите сделать некоторые данные доступными для нескольких компонентов, но не хотите передавать эти данные в виде пропов на каждом уровне дерева компонентов.

Когда следует использовать Context и useReducer()?

Когда вам требуется управление состоянием умеренно сложного компонента в определенной части приложения.

Когда следует использовать Redux?

Redux наиболее полезен в следующих случаях:

  • Большое количество компонентов с состояниями, в которых используются одни и те же данные
  • Состояние приложение часто обновляется
  • Сложная логика обновления состояния
  • Приложение имеет среднюю или большую кодовую базу и над ним работает много людей
  • Вы хотите знать когда, почему и как обновляется состояние приложения и иметь возможность визуализировать эти изменения
  • Вам требуются более мощные возможности по управлению побочными эффектами, стабильностью (постоянством) и сериализацией данных

Понимание Context и Redux

Для правильного использования инструмента критически важно понимать:

  • Для чего он предназначен
  • Какие задачи он решает
  • Когда и зачем он был создан

Неразбериха вокруг Context и Redux связана, в первую очередь, с непониманием того, для чего данные инструменты предназначены, и какие задачи они решают. Поэтому, прежде чем говорить о том, когда их следует использовать, необходимо определить, что они из себя представляют и какие проблемы решают.

Что такое контекст?

Начнем с определения контекста из официальной документации:

«Контекст позволяет передавать данные через дерево компонентов без необходимости передавать пропы на промежуточных уровнях.

В типичном React-приложении данные передаются сверху вниз (от предка к потомку) с помощью пропов. Однако, этот способ может быть чересчур громоздким для некоторых типов пропов (например, выбранный язык, тема интерфейса), которые необходимо передавать во многие компоненты в приложении. Контекст предоставляет способ распределять такие данные между компонентами без необходимости явно передавать пропы на каждом уровне дерева компонентов».

Обратите внимание, в данном определении ни слова не говорится об «управлении», только о «передаче» и «распределении».

Текущее API контекста (React.createContext()) впервые было представлено в React 16.3 в качестве замены устаревшего API, доступного в ранних версиях React, но имеющего несколько недостатков дизайна. Одной из главных проблем являлось то, что обновления значений, переданных через контекст, могли быть «заблокированы», если компонент пропускал рендеринг через shouldComponentUpdate(). Поскольку многие компоненты прибегали к shouldComponentUpdate() в целях оптимизации, передача данных через контекст становилась бесполезной. createContext() был спроектирован для решения этой проблемы, поэтому любое обновление значения отразится на дочерних компонентах, даже если промежуточный компонент пропускает рендеринг.

Использование контекста

Использование контекста в приложении предполагает следующее:

  • Вызываем const MyContext = React.createContext() для создания экземпляра объекта контекста
  • В родительском компоненте рендерим >. Это помещает некоторые данные в контекст. Эти данные могут быть чем угодно: строкой, числом, объектом, массивом, экземпляром класса, обработчиком событий и т.д.
  • Получаем значение контекста в любом компоненте внутри провайдера, вызывая const theContextValue = useContext(MyContext)

Обычно, значением контекста является состояние компонента:

import < createContext >from 'react' export const MyContext = createContext() export function ParentComponent(< children >) < const [counter, setCounter] = useState(0) return ( > ) > 

После этого дочерний компонент может вызвать хук useContext() и прочитать значение контекста:

import < useContext >from 'react' import < MyContext >from './MyContext' export function NestedChildComponent() < const [counter, setCounter] = useContext(MyContext) // . >
Цель и случаи использования контекста

Мы видим, что контекст, в действительности, ничем не управляет. Вместо этого, он представляет собой своего рода тоннель (pipe). Вы помещаете данные в начало (наверх) тоннеля с помощью , затем эти данные опускаются вниз до тех пор, пока компонет не запросит их с помощью useContext(MyContext).

Таким образом, основная цель контекста состоит в предотвращении «бурения пропов» (prop-drilling). Вместо передачи данных в виде пропов на каждом уровне дерева компонентов, любой компонент, вложенный в , может получить к ним доступ посредством useContext(MyContext). Это избавляет от необходимости писать код, реализующий логику передачи пропов.

Концептуально, это является формой внедрения зависимостей. Мы знаем, что потомок нуждается в данных определенного типа, но он не пытается создавать или устанавливать эти данные самостоятельно. Вместо этого, он полагается на то, что некоторый предок передаст эти данные во время выполнения (runtime).

Что такое Redux?

Вот о чем гласит определение из «Основ Redux»:

«Redux — это паттерн проектирования и библиотека для управления и обновления состояния приложения, использующая события, именуемые операциями (действиями, actions). Redux выступает в роли централизованного хранилища состояния приложения, следюущего правилам, позволяющим обеспечить предсказуемое обновление состояние.

Redux позволяет управлять „глобальным“ состоянием — состоянием, которое трубется нескольким частям приложения.

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

Обратите внимание, что данное описание указывает на:

  • Управление состоянием
  • Цель Redux — определение того, почему и как произошло изменение состояния

Архитектурно, Redux подчеркнуто использует принципы функционального программирования, что позволяет писать код в форме предсказуемых «функций-редукторов» (reducers), и обособлять идею «какое событие произошло» от логики, определяющей «как обновляется состояние при возгникновении данного события». В Redux также используется промежуточное программное обеспечение (middleware) как способ расширения возможностей хранилища, включая обработку побочных эффектов (side effects).

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

Redux и React

Сам по себе Redux не зависит от UI — вы можете использовать его с любым слоем представления (view layer) (React, Vue, Angular, ванильный JS и т.д.) либо без UI вообще.

Однако, чаще всего, Redux используется совместно с React. Библиотека React Redux — это официальный связывающий слой UI, позволяющий React-компонентам взаимодействовать с хранилищем Redux, получая значения из состояния Redux и инициализируя выполнение операций. React-Redux использует контекст в своих внутренних механизмах. Тем не менее, следует отметить, что React-Redux передает через контекст экземпляр хранилища Redux, а не текущее значение состояния! Это пример использования контекста для внедрения зависимостей. Мы знаем, что наши подключенные к Redux компоненты нуждаются во взаимодействии с хранилищем Redux, но мы не знаем или нам неважно, что это за хранилище, когда мы определяем компонент. Настоящее хранилище Redux внедряется в дерево во время выполнения с помощью компонента , предоставляемого React-Redux.

Следовательно, React-Redux также может быть использован для предотвращения «бурения» (по причине внутреннего использования контекста). Вместо явной передачи нового значения через , мы можем поместить эти данные в хранилище Redux и затем получить их в нужном компоненте.

Цель и случаи использования (React-)Redux

Основное назначение Redux согласно официальной документации:

«Паттерны и инструменты, предоставляемые Redux, облегчают понимание того, когда, где, почему и как произошло изменение состояния, а также того, как на это отреагировало приложение».

Существует еще несколько причин использования Redux. Одной из таких причин является предотвращение «бурения».

Другие случаи использования:

  • Полное разделение логики управления состоянием и слоя UI
  • Распределение логики управления состоянием между разными слоями UI (например, в процессе перевода приложения с AngularJS на React)
  • Использование возможностей Redux middleware для добавления дополнительной логики при инициализации операций
  • Возможность сохранения частей состояния Redux
  • Возможность получения отчетов об ошибках, которые могут быть воспроизведены другими разработчиками
  • Возможность быстрой отладки логики и UI во время разработки

Почему контекст не является инструментом «управления состоянием»?

Состояние — это любые данные, описывающие поведение приложения. Мы можем разделить состояние на такие категории, как состояние сервера, состояние коммуникаций и локальное состояние, если хотим, но ключевым аспектом является хранение, чтение, обновление и использование данных.

«Управление состоянием — это изменение состояния в течение времени».

Таким образом, мы можем сказать, что «управление состоянием» означает следующее:

  • Сохранение начального значения
  • Получение текущего значения
  • Обновление значения

React-хуки useState() и useReducer() являются отличными примерами управления состоянием. С помощью этих хуков мы можем:

  • Сохранять начальное значение путем вызова хука
  • Получать текущее значение также посредством вызова хука
  • Обновлять значение, вызывая функцию setState() или dispatch(), соответственно
  • Узнавать об обновлении состояния благодаря повторному рендерингу компонента
  • Redux сохраняет начальное значение путем вызова корневого редуктора (root reducer), позволяет читать текущее значение с помощью store.getState(), обновлять значение с помощью store.dispatch(action) и получать уведомления об обновлении состояния через store.subscribe(listener)
  • MobX сохраняет начальное значение путем присвоения значения полю класса хранилища, позволяет читать текущее значение и обновлять его через поля хранилища и получать уведомления об обновлении состояния с помощью методов autorun() и computed()
React Context не соответствует названным критериям. Поэтому он не является инструментом управления состоянием

Как было отмечено ранее, контекст сам по себе ничего не хранит. За передачу значения, которое, обычно, зависит от состояния компонента, в контексте отвечает родительский компонент, который рендерит . Настоящее «управление состоянием» происходит при использовании хуков useState()/useReducer().

«Контекст — это то, как существующее состояние распределяется между компонентами. Контекст ничего не делает с состоянием».

«Полагаю, контекст — это как скрытые пропы, абстрагирующие состояние».

Все, что делает контекст, это позволяет избежать «бурения».

Сравнение Context и Redux

Сравним возможности контекста и React+Redux:

  • Context
    • Ничего не хранит и ничем не управляет
    • Работает только в компонентах React
    • Передает ниже простое (единственное) значение, которое может быть чем угодно (примитивом, объектом, классом и т.д.)
    • Позволяет читать это простое значение
    • Может использоваться для предотвращения «бурения»
    • Показывает текущее значение для компонентов Provider и Consumer в инструментах разработчика, но не показывает историю изменений этого значения
    • Обновляет «потребляющие» компоненты при изменении значения, но не позволяет пропустить обновление
    • Не предоставляет механизма для обработки побочных эффектов — отвечает только за рендеринг
    • Хранит и управляет простым значением (обычно, этим значением является объект)
    • Работает с любым UI, а также за пределами React-компонентов
    • Позволяет читать это простое значение
    • Может использоваться для предотвращения «бурения»
    • Может обновлять значение путем инициализации операций и запуска редукторов
    • Инструменты разработчика показывают историю инициализации операций и изменения состояния
    • Предоставляет возможность использования middleware для обработки побочных эффектов
    • Позволяет компонентам подписываться на обновления хранилища, извлекать определенные части состояния хранилища и контролировать повторный рендеринг компонентов

    Context и useReducer()

    Одной из проблем в дискуссии «Context против Redux» является то, что люди, зачастую, на самом деле имеют ввиду следующее: «Я использую useReducer() для управления состоянием и контекст для передачи значения». Но, вместо этого, они просто говорят: «Я использую контекст». В этом, на мой взгляд, кроется основная причина неразберихи, способствующая поддержанию мифа о том, что контекст «управляет состоянием».

    Рассмотрим комбинацию Context + useReducer(). Да, такая комбинация выглядит очень похоже на Redux + React-Redux. Обе эти комбинации имеют:

    • Сохраненное значение
    • Функцию-редуктор
    • Возможность инициализации операций
    • Возможность передачи значения и его чтения во вложенных компонентах
    • Context + useReducer() основан на передаче текущего значения через контекст. React-Redux передает через контекст текущий экземпляр хранилища Redux
    • Это означает, что когда useReducer() производит новое значение, все компоненты, подписанные на контекст, принудительно перерисовываются, даже если они используют только часть данных. Это может привести к проблемам с производительностью в зависимости от размера значения состояния, количества подписанных компонентов и частоты повторного рендеринга. При использовании React-Redux компоненты могут подписываться на определенную часть значения хранилища и перерисовываться только при изменении этой части
    • Контекст + useReducer() являются встроенными возможностями React и не могут использоваться за его пределами. Хранилище Redux не зависит от UI, поэтому может использоваться отдельно от React
    • React DevTools показывают текущее значение контекста, но не историю его изменений. Redux DevTools показывают все инициализированные операции, их содержимое (тип и полезную нагрузку, type and payload), состояние после каждой операции и разницу между состояниями
    • useReducer() не имеет middleware. Некоторые побочные эффекты можно обработать с помощью хука useEffect() в сочетании с useReducer(), я даже встречал отдельные попытки оборачивания useReducer() в нечто похожее на middleware, однако всему этому далеко до функционала и возможностей Redux middleware

    «Мое личное мнение состоит в том, что новый контекст готов к использованию для маловероятных обновлений с низкой частотой (таких как локализация или тема). Он также может использоваться во всех тех случаях, в которых использовался старый контекст, т.е. для статических значений с последующим распространением обновления по подпискам. Он не готов к использованию в качестве замены Flux-подобных „распространителей“ состояния».

    В сети существует много статей, рекомендующих настройку нескольких отдельных контекстов для разных частей состояния, что позволяет избежать ненужных повторных рендерингов и решить проблемы, связанные с областями видимости. Некоторые из постов также предлагают добавлять собственные «компоненты по выбору контекста», что требует использования сочетания React.memo(), useMemo() и аккуратного разделения кода на два контекста для каждой части приложения (одна для данных, другая для функций обновления). Безусловно, код можно писать и так, но в этом случае вы заново изобретаете React-Redux.

    Таким образом, несмотря на то, что Context + useReducer() — это легкая альтернатива Redux + React-Redux в первом приближении… эти комбинации не идентичны, контекст + useReducer() не может полностью заменить Redux!

    Выбор правильного инструмента

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

    Обзор случаев использования

    • Context
      • Передача данных вложенным компонентам без «бурения»
      • Управление состоянием сложного компонента с помощью функции-редуктора
      • Управление состоянием сложного компонента с помощью функции-редуктора и передача состояния вложенным компонентам без «бурения»
      • Управление очень сложным состоянием с помощью функций-редукторов
      • Прослеживаемость того, когда, почему и как менялось состояние в течение времени
      • Желание полной изоляции логики управления состоянием от слоя UI
      • Распределение логики управления состоянием между разными слоями UI
      • Использование возможностей middleware для реализации дополнительной логики при инициализации операций
      • Возможность сохранения определенных частей состояния
      • Возможность получения воспроизводимых отчетов об ошибках
      • Возможность быстрой отладки логики и UI в процессе разработки
      • Все случаи использования Redux + возможность взаимодействия React-компонентов с хранилищем Redux

      Рекомендации

      Как же решить, что следует использовать?

      Для этого вам всего лишь нужно определить, какой инструмент наилучшим образом решает задачи вашего приложения.

      • Если вам требуется просто избежать «бурения», используйте контекст
      • Если у вас имеется сложное состояние, но вы не хотите использовать сторонние библиотеки, используйте контекст + useReducer()
      • Если вам требуется хорошая трассировка изменений состояния во времени, управлемый повторный рендеринг определенных компонентов, более мощные возможности обработки побочных эффектов и т.п., используйте Redux + React-Redux

      Часто можно услышать, что «использование Redux предполагает написание большого количества шаблонного кода», однако, «современный Redux» значительно облегчает изучение данного инструмента и его использование. Официальный пакет Redux Toolkit решает проблему шаблонизации, а хуки React-Redux упрощают использование Redux в компонентах React.

      Разумеется, добавление RTK и React-Redux в качестве зависимостей увеличивает «бандл» приложения по сравнению с контекстом + useReducer(), которые являются встроенными. Но преимущества такого подхода перекрывают недостатки — лучшая трассировка состояния, простая и более предсказуемая логика, улучшенная оптимизация рендринга компонентов.

      Также важно отметить, что одно не исключает другого — вы можете использовать Redux, Context и useReducer() вместе. Мы рекомендуем хранить «глобальное» состояние в Redux, а локальное — в компонентах и внимательно подходить к определению того, какая часть приложения должна храниться в Redux, а какая — в компонентах. Так что вы можете использовать Redux для хранения глобального состояния, Context + useReducer() — для хранения локального состояния, и Context — для статических значений, одновременно и в одном приложении.

      Еще раз: я не утверждаю, что все состояние приложения должно храниться в Redux или что Redux — это всегда лучшее решение. Я утверждаю лишь, что Redux — хороший выбор, существует много причин использовать Redux, и плата за его использование не так высока, как многие думают.

      Наконец, контекст и Redux не единственные в своем роде. Существует множество других инструментов, решающих иные аспекты управления состоянием. MobX — популярное решение, использующее ООП и наблюдаемые объекты (observables) для автоматического обновления зависимостей. Среди других подходов к обновлению состояния можно назвать Jotai, Recoil и Zustand. Библиотеки для работы с данными, вроде React Query, SWR, Apollo и Urql, предоставляют абстракции, упрощающие применение распространенных паттернов для работы с состоянием, кэшируемым сервером (скоро похожая библиотека (RTK Query) появится и для Redux Toolkit).

      Надеюсь, данная статья помогла вам понять разницу между контекстом и Redux, а также какой инструмент и в каких случаях следует использовать. Благодарю за внимание.

      • Веб-разработка
      • JavaScript
      • Программирование
      • ReactJS

      Что такое контекст c

      Каждая переменная доступна в рамках определенного контекста или области видимость. Вне этого контекста переменная уже не существует.

      Существуют различные контексты:

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

      Контекст метода. Переменные, определенные на уровне метода, являются локальными и доступны только в рамках данного метода. В других методах они недоступны

      Контекст блока кода. Переменные, определенные на уровне блока кода, также являются локальными и доступны только в рамках данного блока. Вне своего блока кода они не доступны.

      Например, пусть класс Program определен следующим образом:

      Здесь определенно четыре переменных: type, name, shortName и surname. Каждая из них существует в своем контексте. Переменная type существует в контексте всего класса Person и доступна в любом месте и блоке кода в методах PrintName и PrintSurname.

      Переменная name существует только в рамках метода PrintName. Также как и переменная surname существует в рамках метода PrintSurname. В методе PrintName мы не можем обратиться к переменной surname , так как она в другом контексте.

      Переменная shortName существует только в блоке кода, границами которого являются открывающая и закрывающая фигурные скобки. Вне его границ переменная shortName не существует и к ней нельзя обратиться.

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

      При работе с переменными надо учитывать, что локальные переменные, определенные в методе или в блоке кода, скрывают переменные уровня класса, если их имена совпадают:

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

      Механизм контекстов в .NET

      Вместе с появлением .NET нам была предложено масса полезных и интересных технологий и одна из них – контексты. Естественно, что контексты не являются чем-то принципиально новым. В том же COM они были доступны, начиная с Windows2000, но популярными они не были. Возможно, с появлением .NET контексты будут использоваться более активно. Почему «возможно», а не «наверняка»? Да потому, что большая часть документации пространства имен System.Runtime.Remoting.Contexts содержит только «This type supports the .NET Framework infrastructure and is not intended to be used directly from your code», то есть классы, входящие в это пространство имён, не документированы. Конечно, нельзя сказать, что за все время существования .NET ничего не изменилось. Например, MSDN Magazine уже напечатал две статьи в мартовских выпусках за 2002 и 2003 годы, и очень вероятно, что в марте 2004 выйдет еще одна статья, посвященная ведению лога вызовов на более высоком уровне.

      Что такое контексты и зачем они нужны?

      Вольный перевод определения контекста из MSDN говорит примерно следующее: «Контекст – это упорядоченный набор свойств, определяющих окружение для объектов, которые исполняются в нем.» Создается контекст на этапе активации объекта, для работы которого нужны те или иные службы (JIT-активация, поддержка транзакций, синхронизация, безопасность и многое другое). Естественно, что в рамках одного контекста может существовать несколько объектов.

      Ядром инфраструктуры контекстов является класс Context (пространство имен System.Runtime.Remoting.Contexts). Рассмотрим его основные методы:

      Context Constructor Конструктор класса. Обычно контексты создаются в процессе активации ContextBoundObject (CBO), но иногда может потребоваться явно создать новый контекст.
      SetProperty Установить свойство контекста.
      GetProperty Получить свойство контекста.
      Freeze Заморозить контекст. «Заморозка» контекста подразумевает, что все нужные свойства контекста уже добавлены, и мы готовы начать его использование.
      DoCallBack Сделать обратный вызов в контекст. Этот метод аналогичен методу AppDomain.DoCallBack с единственным отличием, что вызов будет обрабатываться в другом контексте.
      RegisterDynamicProperty Регистрация динамического свойства.
      UnregisterDynamicProperty Удалить динамическое свойство.

      Есть и несколько других методов, но они не представляют особого интереса (при желании можно посмотреть полный список методов в MSDN).

      Как уже упоминалось, использование контекстов тесно связано с классом ContextBoundObject. Это единственный класс (не считая его наследников), для которого всегда создается контекст исполнения. Естественно, встает вопрос – как управлять созданием контекста и наполнением его свойствами?

      Наверное, все знают о механизме атрибутов в .NET Framework и о том, как их можно использовать для расширения метаинформации о классе. Контексты в этом случае не исключение. Здесь также активно используются атрибуты для описания дополнительных возможностей. И основным атрибутом здесь выступает ContextAttribute. Обычно этот атрибут используется для управления созданием нового контекста и определения свойств создаваемого контекста. К атрибутам и свойствам мы вернемся чуть позже, а пока рассмотрим, как обеспечивается взаимодействие с CBO.

      Работа с CBO в .NET Framework построена на использовании связки TransparentProxy и RealProxy. И здесь программист может столкнуться с уже упоминавшейся трудностью – отсутствием документации. Если RealProxy еще худо-бедно документирован, то с TransparentProxy дела обстоят хуже. В MSDN есть несколько незначительных упоминаний о том, что он существует, и что обычному программисту он не нужен. TP трудно назвать полноценным классом – это, скорее, некий мост между средой исполнения и управляемым кодом. Поэтому легко можно обойтись без документации, так как единственное, что необходимо помнить – это то, что в нем хранится ссылка на RealProxy и некий объект под названием StubData, который используется для принятия решения о том, какой из механизмов вызова методов CBO-объекта будет использоваться (рисунок 1).

      Естественно, что это создает один из побочных эффектов – все (sic!) обращения к CBO идут через TransparentProxy (TP), что вносит накладные расходы.

      Но это все относится к механизмам работы с ContextBoundObject, а пока мы вернемся к самому началу и рассмотрим механизм создания CBO. Поговорим и о том, на основе какой информации принимается решение о создании для объекта нового контекста или использовании существующего контекста.

      Каждый раз, когда исполняемая среда создаёт объект, требующий управляемой активации, она проверяет наличие у этого объекта атрибутов контекста. У этих атрибутов запрашивается два специальных интерфейса – IContextAttribute и IСontextProperty (ContextBoundAttribute реализует оба этих интерфейса). Интерфейс IContextAtrtribute используется для принятия решения о создании нового контекста и наполнении его свойствами, а IContextProperty используется для описания свойств контекста. Активация любого ContextBound-объекта производится в несколько этапов (рисунок 2).

      IContextAttribute.IsContextOK – каждый атрибут проверяет пригодность текущего контекста для активации. Если хоть один атрибут возвращает false, то принимается решение о создании нового контекста;

      IContextAttribute.GetPropertiesForNewContext – если было принято решение о создании нового контекста, каждый атрибут получает возможность добавить в новый контекст свой список свойств;

      IContextProperty.Freeze – каждое свойство уведомляется о том, что инициализация контекста завершена, и оно должно зафиксировать свое состояние;

      IContextProperty.IsNewContextOK – после фиксации состояния контекста каждое свойство получает возможность проверить корректность созданного контекста и если, все в порядке, то дальше начинается процесс активации объекта.

      Обычно интерфейсы IContextAttribute и IContextProperty напрямую не используются, а используется класс ContextAttribute реализующий оба этих интерфейса.

      СОВЕТ

      Естественно, что использование ContextBoundAttribute при объявлении класса не является обязательным требованием. Мы всегда можем воспользоваться классом Activator и указать нужные атрибуты в методе CreateInstance, или самостоятельно создать новый контекст и форсировать активацию CBO именно в нем.

      Кроме интерфейса IContextProperty каждое свойство может реализовать и несколько дополнительных интерфейсов, каждый из которых так или иначе может влиять на процесс взаимодействия с ContextBoundObject:

      IContributeClientContextSink Интерфейс IContributeClientContextSink дает возможность свойству принять участие в создании клиентского приемника (IMessageSink) контекста. Клиентский приемник сообщений создается в момент первого вызова из-за границы контекста и может использоваться для перехвата всех исходящих вызовов
      IContributeServerContextSink Интерфейс IContributeServerContextSink дает возможность свойству принять участие в создании серверного приемника. Серверный приемник сообщений создается в момент первого входа в контекст и может использоваться для перехвата всех входящих в контекст вызовов.
      IContributeEnvoySink Интерфейс IContributeEnvoySink дает возможность свойству принять участие в создании «дипломатического» приемника. «Дипломатический» приемник создается в серверном контексте, передается на сторону клиента (именно поэтому необходимо, чтобы класс, реализующий данный приемник, поддерживал сериализацию) и позволяет воздействовать на цепочку обработки сообщений еще до того, как произойдет переход из клиентского контекста в серверный. Одно из возможных назначений EnvoySink – это контроль входных параметров объекта, интеллектуальное кэширование вызовов и т.д. EnvoySink привязывается к конкретному объекту и служит для обработки сообщений конкретного объекта.
      IContributeObjectSink Интерфейс IContributeObjectSink дает возможность свойству принять участие в создании приемника «объекта». Можно сказать, что этот приемник является серверным аналогом EnvoySink. Он так же, как и EnvoySink, привязывается к конкретному объекту и служит для обработки сообщений конкретного объекта.
      IContextPropertyActivator Интерфейс IContextPropertyActivator дает возможность свойству влиять на процесс активации объекта.

      На процесс взаимодействия с ContextBoundObject могут оказывать свое влияние и динамические свойства (IDynamicProperty). В отличие от обычных свойств, динамические свойства могут подключаться и отключаться в процессе существования контекста (методы Context.RegisterDynamicProperty и Context.UnregisterDynamicProperty):

      IContributeDynamicSink Интерфейс IContributeDynamicSink дает возможность динамическому свойству создать IDynamicMessageSink. В целом возможности, предоставляемые посредством динамического приемника, схожи с теми, что можно получить через IContextPropertyActivator. Основное отличие между ними заключается в том, что IContextPropertyActivator действует только на этапе активации объекта и имеет более высокий приоритет.

      Теперь рассмотрим использование свойств, входящих в состав контекста (для простоты будем рассматривать только синхронные вызовы).

      Начнем мы это делать с серверного контекста. Почему с серверного? Конечно, можно было бы начать и с клиентского, но именно свойства серверного контекста обычно задействуются первыми. Отчего это происходит именно так – достаточно вспомнить с чего начинается исполнение большинства программ:

      Что получается, когда управление попадает в Main? Естественно, создаётся контекст, используемый по умолчанию, у которого клиентские свойства не определены. Когда будет создан первый ContextBoundObject, то первыми начнут работать свойства именно его (т.е. серверного контекста).

      Активизация CBO в серверном контексте

      Будем считать, что для своего первого CBO мы не поскупились на всевозможные свойства и решили использовать все доступные нам возможности. Что при этом будет происходить, изображено на рисунке 4. Процесс активации объекта начинается в методе DoCrossContextActivation класса ActivationServices (к сожалению, данный класс объявлен как internal, и поэтому его использование сильно затруднено). Первым делом в этом методе создается и инициализируется новый контекст. После этого контекст замораживается (IContextProperty.Freeze), и каждое свойство получает возможность проверить состояние нового контекста (блок IContextProperty на рисунке 2). После проверки контекста инициируется процесс создания «серверного приемника». Для этого каждое свойство проверяется на предмет поддержки интерфейса IContributeServerContextSink и, если такая поддержка обнаруживается, то создается цепочка «приемников», которая завершается специальным приемником ServerContextTerminatorSink.

      После формирования цепочки «серверных приемников» вызов переходит к первому приемнику в цепочке и, следуя по ней, достигает последнего – ServerContextTerminatorSink, который уведомляет свойства контекста, поддерживающие интерфейс IСontextPropertyActivator, и передает вызов активатору объекта — IConstructionCallMessage.Activator.Activate. Обычно (если никто ничего не поменял на предыдущем шаге) активатором выступает внутренний класс ConstructionLevelActivator, обеспечивающий конструирование объекта и создание его «дипломатических приемников». Так же, как и для «серверных приемников», это происходит через опрос свойств контекста, только в данном случае используется интерфейс IContributeEnvoySink.

      Что является здесь наиболее важным? Во-первых, получая вызов IContributeServerContextSink.GetServerContextSink, мы можем быть уверены, что создаваемый объект является в этом контексте «первым». Во вторых, именно на этапе активации создается EnvoySink объекта и, хотя вызова SyncProcessMessage для EnvoySink в серверном контексте не будет, именно на этапе активации объект, реализующий EnvoySink, будет отправлен в клиентский контекст (вполне возможно, что его даже придется сериализовать).

      Естественно, что в процессе серверной активации некоторые свойства оказались незадействованными. Разумеется, не все можно сделать в серверном контексте, и иногда более удобно выполнить часть работы в своем «родном» контексте. Поэтому вернемся и рассмотрим, что же происходило в клиентском контексте до того, как мы начали активацию в серверном.

      Активация CBO в клиентском контексте

      Активация в клиентском контексте (рисунок 5) начинается после того, как было принято решение о создании нового контекста. Это происходит непосредственно после того, как были опрошены все атрибуты, и один из них (IContextAttribute.IsContextOK) вернул false. Естественно, что еще до начала активации была создана «парочка» TransparentProxy и RealProxy, и именно благодаря действиям RealProxy запускается механизм создания нового контекста (вообще-то, RealProxy — это абстрактный класс, в реальности используется недокументированный класс RemotingProxy)

      Так или иначе, будем считать, что активация CBO в клиентском контексте начинается с создания цепочки «клиентских приемников». Для этого используются свойства контекста с интерфейсом IContributeClientSink (стоит отметить, что IContributeClientSink используется только один раз, в тот момент, когда управление передается за границу клиентского контекста). После обработки сообщения в клиентском приемнике мы попадаем в ClientContextTerminatorSink, который «уведомляет» динамические свойства контекста и IContextPropertyActivator о вызове. Последним этапом обработки сообщения в клиентском контексте является специальный объект ContextLevelActivator. Именно на этом этапе осуществляется проверка свойств (рисунок 2) вновь созданного серверного контекста – IContextProperty.Freeze & IContextProperty.IsNewContextOK.

      Но на этом работа со свойствами контекста еще не завершена. Часть свойств обрабатывается при первом вызове метода CBO.

      Вызов метода CBO в клиентском контексте

      Когда рассматривался процесс активизации объекта в серверном контексте, был упомянут специальный приемник – EnvoySink. Как уже говорилось – этот приемник был создан в процессе «серверной» активации и именно он получит шанс первым обработать сообщение на стороне клиента. Такое «поведение» делает “дипломатические” приемники очень удобным инструментом для контроля/модификации входных параметров. Наиболее полезным это может оказаться в распределенных приложениях. В этом случае неверные данные даже не будут переданы на сервер (нужно учитывать, что есть возможность обойти вызов EnvoySink, поэтому наиболее эффективным будет дублирование проверяющего кода как в EnvoySink, так и в ObjectSink).

      Вызов метода CBO объекта в серверном контексте

      Выйдя за пределы клиентского контекста, вызов проходит через уже созданную цепочку серверных приемников и попадает в ServerContextTerminatorSink (рисунок 7). Этот стандартный приемник инициирует создание цепочки «объектных приемников», которая будет завершена приемником ServerObjectTerminatorSink (как и во всех остальных случаях, приемники создаются с помощью свойств контекста, только в данном случае будет использоваться IContributeObjectSink).

      Что было пропущено на этой диаграмме? Для упрощения можно считать, что вызов методов CBO-объекта осуществляется напрямую из ServerObjectTerminatorSink. В реальности же есть еще один дополнительный приемник – это StackBuilderSink, и именно он отвечает за доставку вызова к методам объекта. Также была пропущена одна интересная возможность – CBO сам может реализовать IMessageSink, и тогда реализация из ServerObjectTerminatorSink не будет использовать StackBuilderSink, а вызовет метод SyncProcessMessage у CBO-объекта.

      СОВЕТ

      Может возникнуть вопрос – если нет возможности вызвать StackBuilderSink (например, если CBO-объект реализовал IMessageSink), то как обратиться к объекту, имея на руках только IMessage? В этом случае все просто – метод RemotingServices.ExecuteMessage обеспечивает практически «прямой» вызов StackBuilderSink.

      Естественно, что может возникнуть вопрос, для чего нужны TerminatorSink и с чем связано такое название. TerminatorSink – это специальный приемник, который завершает обработку сообщений на определенном уровне и служит неким гарантом того, что он на этом «уровне» всегда будет последним (именно поэтому его свойство IMessageSink.NextSink всегда возвращает null. Таким образом, его просто невозможно пропустить).

      Что осталось еще?

      Особенно радует, что поддержка контекстов в .NET не ограничивается абстрактными интерфейсами. Существуют и специальные атрибуты ContextStaticAttribute и SynchronizationAttribute, их достаточно подробное описание и назначение можно найти в MSDN.

      Также заслуживает отдельного упоминания специальный атрибут ProxyAttribute. Использование этого атрибута позволяет перехватить момент создания класса специального класса RealProxy и при необходимости использовать вместо стандартной реализации собственную. Бесспорно, что это очень интересная возможность, единственная причина, по которой мы не будем ее рассматривать, заключается в том, что к контекстам она не имеет никакого отношения. Достаточно посмотреть, с чего начинается работа с контекстом (рисунок 5). Если мы создаем собственную реализацию RealProxy, то мы теряем практически всю инфраструктуру (очень сомнительно, что кто-то сможет написать собственную реализацию того, что было показано на рисунках 4 – 7).

      Примеры использования контекстов

      В любом случае описание возможностей контекстов не так интересно, если нет примеров их использования (ведение лога вызов не в счет). И, как сейчас будет показано, предоставляемые контекстами возможности достаточно широки.

      Первую задачу которую мы попробуем решить, используя контексты, – это задача «перехвата» создания MarshalByRefObject. Учитывая, что это всего лишь пример, упростим себе задачу и ограничимся простой подменой типа. Основой для теста будут классы Source и Derived

      Перед решением этой задачи нужно будет ответить на вопрос – а только ли на наследников ContextBoundObject распространяется действие свойств контекста? Нет, не только. Действие контекста распространяется и на наследников класса MarshalByRefObject (естественно, что только для случая, когда класс зарегистрирован как remote-класс). Разумеется, не стоит надеяться, что будут доступны все возможности контекстов, но и тех, которые доступны, нам хватит.

      С чего стоит начать? Для этого нужно посмотреть на рисунок 5. ClientContextSink получает управление в клиентском контексте первым. Единственное, свойства контекста по умолчанию мы изменить не можем. Ничего не поделаешь, придется создавать свой контекст.

      Первым, что понадобится, – это реализация свойства контекста с интерфейсом IContributeClientContextSink, который создаст нам нужную реализацию ClientContextSink

      Помимо атрибута, придется реализовать и ClientContextSink, специальный «приемник», который возьмет на себя подмену объектов. Именно он проделает всю основную работу по перехвату.

      Не будем вдаваться в подробности того, как осуществляется подключение экземпляра объекта к Proxy. Вместо этого приведем диаграмму связей TransparentProxy, RealProxy и MBR объекта (рисунок 3).

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

      Автоматические транзакции средствами контекстов

      Следующий пример имеет более практическую направленность — мы увидим, каким образом можно добавить к объекту поддержку автоматических транзакций, используя контексты.

      В .NET уже встроена поддержка автоматических транзакций, эту задачу решает класс System.EnterpriseServices.ServicedComponent. К сожалению, единственное, что мешает его полноценно использовать – тесная интеграция с COM, и, как следствие, необходимость регистрации компонентов в каталоге COM+,что, разумеется, сильно ограничивает возможности распространения подобных приложений. И что особенно огорчает – использование ServicedComponent – это практически единственный способ получить поддержку автоматических транзакций в .NET

      ПРИМЕЧАНИЕ

      Примечание: Поддержку автоматических транзакций можно также получить через ASP.NET или Web-сервис, но это не всегда удобно. Еще один вариант – начиная с .NET Framework 1.1 появилось несколько специальных классов, позволяющих использовать возможности COM+ 1.5, но по каким-то причинам эти классы доступны только под Windows Server 2003.

      Итак, мы можем получить поддержку транзакций для своего объекта, воспользовавшись для этого сервисами, предоставляемыми COM+. Для этого можно использовать COM+ 1.5 и его интерфейсы (CoCreateActivity, IServiceCall, IServiceActivity и т.д.), но в этом случае мы вынуждены будем работать в WinXP или в Win2003, что само по себе является достаточно серьёзным ограничением. Можно также написать специальный библиотечный COM+-компонент, который «поделится» с нами своими сервисами. Правда, может показаться, что в этом случае мы от чего ушли, к тому и пришли, – устанавливать компонент в каталог COM+ все равно нужно. Но это не совсем так, ибо – установка такого компонента сродни установке .NET Framework её нужно будет произвести только единожды, в дальнейшем мы сможем ее использовать из любого приложения. Да и возможность работы в Win2000 будет тоже неплохим плюсом.

      Попробуем сразу определить, что же мы хотим получить от нашей реализации. Естественно, что реализовать все возможности ServicedComponent в рамках одного примера нельзя, поэтому ограничимся самой простой реализацией:

      Первое, что нам нужно будет решить, — это на каком этапе будут добавляться службы контекста. В данном случае нужно предоставлять службы транзакций для конкретного экземпляра объекта. Ответив на этот вопрос можно нарисовать примерную схему (рисунок 8) того, как должны осуществляться вызовы методов нашего класса. А теперь, сама реализация:

      Это, естественно, не весь код, некоторые незначительные классы и реализация COM+-компонента не приведены (весь код целиком можно посмотреть в прилагаемом архиве). Но основная идея проста – это перехват сообщений о вызове методов ContextBoundObject и обработка их уже в контексте COM+.

      Заключение.

      Контексты в .NET – это очень интересная технология, грамотное использование которой может существенно облегчить жизнь разработчику. И, естественно, возможности контекстов не ограничиваются ведением логов и обработкой транзакций, каждый разработчик может найти для себя интересное решение.

      Context Класс

      Некоторые сведения относятся к предварительной версии продукта, в которую до выпуска могут быть внесены существенные изменения. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.

      Определяет среду для объектов, которые находятся внутри нее и для которых может быть реализована политика.

      Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода.

      Комментарии

      Контекст — это упорядоченная последовательность свойств, которые определяют среду для объектов, находящихся внутри него. Контексты создаются во время процесса активации для объектов, настроенных для использования определенных автоматических служб, таких как синхронизация, транзакции, JIT-активация, безопасность и т. д. Внутри контекста возможно существование многих объектов.

      Классы помечаются экземпляром ContextAttribute класса, который предоставляет правила использования. при создании экземпляра нового объекта платформа .NET Framework находит совместимый объект или создает новый экземпляр Context класса для объекта. После помещения объекта в контекст он остается в течение всего времени. Классы, которые могут быть привязаны к контексту, называются контекстно-привязанными классами. При доступе из другого контекста такие классы указываются непосредственно с помощью прокси-сервера. Любой вызов объекта в одном контексте объекту в другом контексте будет проходить через прокси-сервер контекста и будет влиять на политику, принудительно применяемую объединенными свойствами контекста.

      Контекст нового объекта обычно выбирается на основе атрибутов meta-данных в классе. Этот механизм является расширяемым с помощью настраиваемых атрибутов. Они называются свойствами статического контекста, которые компилируются в метаданные класса. Свойства динамического контекста (также известные как свойства конфигурации) могут быть применены и настроены администраторами.

      Дополнительные сведения о контекстах см. в разделе границы: процессы и домены приложений.

      Конструкторы

      Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода.

      Инициализирует новый экземпляр класса Context.

      Свойства

      Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода.

      Возвращает идентификатор текущего контекста.

      Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода.

      Возвращает массив текущих контекстных свойств.

      Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода.

      Возвращает контекст по умолчанию для текущего домена приложения.

      Методы

      Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода.

      Выделяет неименованную область данных.

      Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода.

      Выделяет именованную область данных.

      Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода.

      Выполняет код в другом контексте.

      Определяет, равен ли указанный объект текущему объекту.

      Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода.

      Очищает вспомогательные объекты для контекстов не по умолчанию.

      Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода.

      Освобождает именованную область данных во всех контекстах.

      Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода.

      Замораживает контекст, делая невозможным добавление и удаление контекстных свойств из текущего контекста.

      Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода.

      Получает значение из указанной области данных текущего контекста.

      Служит хэш-функцией по умолчанию.

      Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода.

      Ищет именованную область данных.

      Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода.

      Возвращает контекстное свойство, указанное именем.

      Возвращает объект Type для текущего экземпляра.

      Создает неполную копию текущего объекта Object.

      Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода.

      Регистрирует динамическое свойство, реализующее интерфейс IDynamicProperty с помощью службы удаленного взаимодействия.

      Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода.

      Помещает данные в указанную область данных текущего контекста.

      Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода.

      Устанавливает значение контекстного свойства, указанного именем.

      Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода.

      Возвращает представление текущего контекста объектом класса String.

      Этот API поддерживает инфраструктуру продукта и не предназначен для использования непосредственно из программного кода.

      Отменяет регистрацию динамического свойства, реализующего интерфейс IDynamicProperty.

      Похожие публикации:

      1. Что такое интернет wlan на роутере мегафон
      2. Что такое маршрут по умолчанию
      3. Что такое паровой цикл у стиральной машины
      4. Что такое рекордер zoom h4n

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *