Ошибки компоновщика могут произойти, если заголовочный файл и связанная с ним общая библиотека (файл .lib) не синхронизируются. Позволь мне объяснить.
Как работают линкеры? Линкер соответствует объявлению функции (объявленному в заголовке) с его определением (в общей библиотеке) путем сравнения их подписи. Вы можете получить ошибку компоновщика, если компоновщик не найдет определение функции, которое идеально подходит.
Возможно ли получить ошибку компоновщика, даже если объявление и определение, похоже, совпадают? Да! Они могут выглядеть одинаково в исходном коде, но это действительно зависит от того, что видит компилятор. По сути, вы можете столкнуться с такой ситуацией:
// header1.h
typedef int Number;
void foo(Number);
// header2.h
typedef float Number;
void foo(Number); // this only looks the same lexically
Обратите внимание, что хотя обе декларации функций выглядят одинаково в исходном коде, но они действительно различаются в зависимости от компилятора.
Вы можете спросить, как это получается в такой ситуации? Включите пути, конечно! Если при компиляции разделяемой библиотеки путь include приводит к header1.h
, и вы в конечном итоге используете header2.h
в своей собственной программе, вы оставите царапины на своем заголовке, задаваясь вопросом, что произошло (каламбур).
Пример того, как это может произойти в реальном мире, объясняется ниже.
У меня есть два проекта: graphics.lib
и main.exe
. Оба проекта зависят от common_math.h
. Предположим, что библиотека экспортирует следующую функцию:
// graphics.lib
#include "common_math.h"
void draw(vec3 p) { ... } // vec3 comes from common_math.h
И затем вы идете вперед и включаете библиотеку в свой собственный проект.
// main.exe
#include "other/common_math.h"
#include "graphics.h"
int main() {
draw(...);
}
Boom! Вы получаете ошибку компоновщика, и вы понятия не имеете, почему она терпит неудачу. Причина в том, что общая библиотека использует разные версии одного и того же include common_math.h
(я сделал это очевидным здесь в этом примере, включив другой путь, но это может быть не всегда так очевидно. Возможно, путь include отличается в настройки компилятора).
Обратите внимание, что в этом примере компоновщик сказал бы вам, что не смог найти draw()
, когда на самом деле вы знаете, что он явно экспортируется библиотекой. Вы могли часами царапать себе голову, думая, что пошло не так. Дело в том, что компоновщик видит другую подпись, потому что типы параметров немного отличаются. В этом примере vec3
является другим типом в обоих проектах в отношении компилятора. Это может произойти из-за того, что они происходят из двух немного разных файлов include (возможно, включенные файлы поступают из двух разных версий библиотеки).
DUMPBIN - ваш друг, если вы используете Visual Studio. Я уверен, что другие компиляторы имеют другие подобные инструменты.
Процесс выглядит следующим образом:
[1] По проекту я имею в виду набор исходных файлов, которые связаны друг с другом для создания либо библиотеки, либо исполняемого файла .
РЕДАКТИРОВАТЬ 1: Переписать первый раздел, который будет легче понять. Пожалуйста, прокомментируйте ниже, чтобы сообщить мне, нужно ли что-то еще исправлять. Спасибо!
Да - возьмите копию переменной внутри цикла:
while (variable < 5)
{
int copy = variable;
actions.Add(() => copy * 2);
++ variable;
}
Вы можете думать об этом, как если бы компилятор C # создавал «новую» локальную переменную каждый раз, когда он попадает в объявление переменной. На самом деле он создаст соответствующие новые объекты закрытия, и он становится сложным (с точки зрения реализации), если вы ссылаетесь на переменные в нескольких областях, но он работает:)
Обратите внимание, что более распространенное появление этого проблема заключается в использовании for
или foreach
:
for (int i=0; i < 10; i++) // Just one variable
foreach (string x in foo) // And again, despite how it reads out loud
Подробнее об этом см. в разделе 7.14.4.2 спецификации C # 3.0, а моя статья о замыканиях еще несколько примеров.
Да, вам нужна область видимости variable
в цикле и передать ее лямбда таким образом:
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
int variable1 = variable;
actions.Add(() => variable1 * 2);
++variable;
}
foreach (var act in actions)
{
Console.WriteLine(act.Invoke());
}
Console.ReadLine();
Это поведение срабатывает, потому что вы используете лямбда-выражение () => variable * 2
, где внешняя область variable
фактически не определена во внутренней области лямбда.
Лямбда-выражения (в C # 3 +, а также анонимные методы в C # 2) по-прежнему создают реальные методы. Передача переменных этим методам связана с некоторыми дилеммами (передать по значению? Pass по ссылке? C # идет по ссылке - но это открывает еще одну проблему, когда ссылка может пережить реальную переменную). Что C # для решения всех этих дилемм заключается в создании нового вспомогательного класса («замыкание») с полями, соответствующими локальным переменным, используемым в лямбда-выражениях, и методам, соответствующим фактическим лямбда-методам. Любые изменения в variable
в вашем коде фактически преобразуются для изменения в этом ClosureClass.variable
. Таким образом, ваш цикл while обновляет ClosureClass.variable
до тех пор, пока он не достигнет 10, тогда вы для циклов выполняете действия, которые все работают на одном и том же ClosureClass.variable
.
Чтобы получить ожидаемый результат, вам необходимо создать разделение между переменной цикла и переменной, которая закрывается. Вы можете сделать это, введя другую переменную, то есть:
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
var t = variable; // now t will be closured (i.e. replaced by a field in the new class)
actions.Add(() => t * 2);
++variable; // changing variable won't affect the closured variable t
}
foreach (var act in actions)
{
Console.WriteLine(act.Invoke());
}
Вы также можете переместить замыкание на другой метод для создания этого разделения:
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
actions.Add(Mult(variable));
++variable;
}
foreach (var act in actions)
{
Console.WriteLine(act.Invoke());
}
Вы можете реализовать Mult как лямбда-выражение (неявное замыкание)
static Func<int> Mult(int i)
{
return () => i * 2;
}
или с фактическим вспомогательным классом:
public class Helper
{
public int _i;
public Helper(int i)
{
_i = i;
}
public int Method()
{
return _i * 2;
}
}
static Func<int> Mult(int i)
{
Helper help = new Helper(i);
return help.Method;
}
В любом случае «Closures» не являются концепцией, связанной с циклами, но скорее, к анонимным методам / лямбда-выражениям используют локальные переменные с областью - хотя некоторые неосторожное использование циклов демонстрируют закрытие ловушек.
То же самое происходит в многопоточном режиме (C #, .NET 4.0].
См. следующий код:
Цель печати 1,2,3,4,5 в порядке.
for (int counter = 1; counter <= 5; counter++)
{
new Thread (() => Console.Write (counter)).Start();
}
Результат интересный! (Это может быть как 21334 ...)
Единственное решение - использовать локальные переменные.
for (int counter = 1; counter <= 5; counter++)
{
int localVar= counter;
new Thread (() => Console.Write (localVar)).Start();
}
За кулисами компилятор генерирует класс, который представляет собой замыкание для вызова метода. Он использует этот единственный экземпляр класса закрытия для каждой итерации цикла. Код выглядит примерно так: это облегчает просмотр ошибки:
void Main()
{
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
var closure = new CompilerGeneratedClosure();
Func<int> anonymousMethodAction = null;
while (closure.variable < 5)
{
if(anonymousMethodAction == null)
anonymousMethodAction = new Func<int>(closure.YourAnonymousMethod);
//we're re-adding the same function
actions.Add(anonymousMethodAction);
++closure.variable;
}
foreach (var act in actions)
{
Console.WriteLine(act.Invoke());
}
}
class CompilerGeneratedClosure
{
public int variable;
public int YourAnonymousMethod()
{
return this.variable * 2;
}
}
На самом деле это не скомпилированный код из вашего примера, но я изучил свой собственный код и этот очень похоже на то, что на самом деле генерирует компилятор.
Я считаю, что то, что вы испытываете, - это что-то известное как Closure http://en.wikipedia.org/wiki/Closure_ (computer_science) . Ваша lamba имеет ссылку на переменную, которая находится вне самой функции. Ваш lamba не интерпретируется до тех пор, пока вы его не вызовите, и как только он получит значение, которое имеет переменная во время выполнения.
Способ сохранения значения, необходимого вам в прокси-переменной, и получить эту переменную.
I.E.
while( variable < 5 )
{
int copy = variable;
actions.Add( () => copy * 2 );
++variable;
}