Microsoft MVP성태의 닷넷 이야기
.NET Framework: 443. 자바 8과 C#의 람다(Lambda) 지원에 대한 비교 [링크 복사], [링크+제목 복사]
조회: 23445
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 2개 있습니다.)

자바 8과 C#의 람다(Lambda) 지원에 대한 비교

자바가 드디어 람다 지원을 하게 되었군요. ^^

그런데, 개인적으로 C#과 어떤 부분이 다를까 궁금해서 좀 찾아봤습니다. 이를 위해 자바 8 개발환경을 구성해야 하는데, 다음과 같은 준비가 필요합니다.

  1. JDK/JRE 8 설치
  2. Eclipse Kepler SR2 설치
  3. Eclipse 내의 Marketplace에서 "Java 8 support for Eclipse Kepler SR2" 플러그인 설치

구성이 끝났으면, 이제 ^^ 간단한 것부터 한번 테스트 해볼까요? ^^

package testapp;

public class Lambda {

    public static void main(String[] args) {
        new Thread(() -> { System.out.println("Hello World!"); }).start();
    }
}

오~~~! C#의 람다 문법과 비슷해서 C# 개발자의 경우 직관적으로 사용할 정도입니다. 위의 코드를 C#으로 옮기면 그 유사성을 실감할 수 있습니다.

using System;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        new Thread(() => { Console.WriteLine("Hello World!"); }).Start();
    }
}

단지 화살표 하나냐(->), 두개냐(=>)의 차이일 뿐입니다. 람다 내부의 코드가 한 줄인 경우 자바나 C#이나 괄호({, })를 생략할 수 있다는 점도 동일합니다.

package testapp;

public class Lambda {

    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello World!") ).start();
    }
}

using System;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        new Thread(() => Console.WriteLine("Hello World!") ).Start();
    }
}




C#의 경우, 메서드를 받을 수 있는 delegate 타입이 있기 때문에 람다 구현이 자연스럽지만, 듣기로는 JVM의 경우 delegate 타입에 대한 확장이 (현실적으로) 불가능하기 때문에 람다 표현식을 자바에서는 어떻게 구현한 것일까 궁금해졌습니다. 예상했던 대로 자바는 역시나 인터페이스를 이용한 방식으로 해결하고 있었습니다. 이름하여 "함수형 인터페이스"라는 것인데 인터페이스에 "단 하나의 메서드 정의"만 담고 있는 것을 의미합니다.

이를 위해 자바 8에서는 함수형 인터페이스라는 "java.lang.FunctionalInterface" 어노테이션이 추가되었는데,

@java.lang.FunctionalInterface
public abstract interface java.lang.Runnable {
  
  // Method descriptor #1 ()V
  public abstract void run();
}

그다지 의미는 없다고 합니다. FunctionalInterface 어노테이션의 유무에 상관없이 메서드 하나만 정의되어 있다면 함수형 인터페이스로 인정받아 컴파일이 됩니다.

예를 들어, 덧셈을 수행하는 람다 표현식을 사용하고 싶은 경우 자바는 다음과 같이 구현할 수 있습니다.

package testapp;

public class Lambda {
    
    interface IAdd
    {
        public int Add(int a, int b);
    }

    public static void main(String[] args) 
    {        
        IAdd func = (a, b) -> a + b;
        System.out.println(func.Add(10, 20));
    }
}

그리고 C#은 이렇게 구현합니다.

using System;

class Program
{
    delegate int AddFunc(int a, int b);

    static void Main(string[] args)
    {
        AddFunc func = (a, b) => a + b;
        Console.WriteLine(func(10, 20));    
    }
}

보시는 바와 같이 자바의 경우 인터페이스(IAdd) 타입의 func 인스턴스에서 다시 Add 메서드를 호출하고 있습니다. 아마도 람다 표현식이란 결국 내부적으로 해당 인터페이스를 상속받은 임시 객체를 생성한 후 그것을 반환하는 것이 아닌가 예상해 봅니다. (조금 뒤에 이에 대해 더 살펴보겠습니다.)

어쨌든, 인터페이스를 경유하는 자바의 구현이 C#과 비교해 깔끔하지 못한 것은 delegate 타입의 부재에서 발생합니다.




그렇다면 실제로 어떻게 구현하고 있는지 바이트 코드 수준에서 한번 살펴볼까요? ^^

IAdd 인터페이스로 구현한 .class 파일을 javap로 역어셈블하면 다음과 같은 코드가 나옵니다.

E:\>E:\java\jdk1.8.0_05\bin\javap -p -c -v  Lambda.class
Classfile /E:/java/workspace/testapp/bin/testapp/Lambda.class
...[생략]...
  BootstrapMethods:
    0: #54 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
      Method arguments:
        #55 (II)I
        #58 invokestatic testapp/Lambda.lambda$0:(II)I
        #59 (II)I
  InnerClasses:
       public static final #65= #61 of #63; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
       static #66= #27 of #1; //IAdd=class testapp/Lambda$IAdd of class testapp/Lambda
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Class              #2             //  testapp/Lambda
...[생략]...
  #16 = NameAndType        #17:#18        //  Add:()Ltestapp/Lambda$IAdd;
  #17 = Utf8               Add
  #18 = Utf8               ()Ltestapp/Lambda$IAdd;
  #19 = InvokeDynamic      #0:#16         //  #0:Add:()Ltestapp/Lambda$IAdd;
 ...[생략]...
  #54 = MethodHandle       #6:#48         //  invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #55 = MethodType         #30            //  (II)I
  #56 = Methodref          #1.#57         //  testapp/Lambda.lambda$0:(II)I
  #57 = NameAndType        #41:#30        //  lambda$0:(II)I
  #58 = MethodHandle       #6:#56         //  invokestatic testapp/Lambda.lambda$0:(II)I
  #59 = MethodType         #30            //  (II)I
...[생략]...
{
...[생략]...

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=2, args_size=1
         0: invokedynamic #19,  0             // InvokeDynamic #0:Add:()Ltestapp/Lambda$IAdd;
         5: astore_1
         6: getstatic     #20                 // Field java/lang/System.out:Ljava/io/PrintStream;
         9: aload_1
        10: bipush        10
        12: bipush        20
        14: invokeinterface #26,  3           // InterfaceMethod testapp/Lambda$IAdd.Add:(II)I
        19: invokevirtual #31                 // Method java/io/PrintStream.println:(I)V
        22: return
      LineNumberTable:
...[생략]...

  private static int lambda$0(int, int);
    descriptor: (II)I
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: iload_0
         1: iload_1
         2: iadd
         3: ireturn
      LineNumberTable:
...[생략]...
}

큰일이군요. ^^ 자바 쪽 역어셈블 경험이 없어서 분석에 자신이 없습니다. 일단, 직관적으로 가보겠습니다. "IAdd func = (a, b) -> a + b;" 코드에 해당하는 라인을 시작으로,

invokedynamic #19

자바 7부터 추가된 invokedynamic 바이트 코드를 이용해 "#19 = InvokeDynamic #0:#16" 항목으로 연결됩니다. 여기서 #0은 부트스트랩 메서드로 등록된 것중 번호가 0번인 항목을 가리키고,
  BootstrapMethods:
    0: #54 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
      Method arguments:
        #55 (II)I
        #58 invokestatic testapp/Lambda.lambda$0:(II)I
        #59 (II)I

이와 함께 #16에 해당하는 "Add:()Ltestapp/Lambda$IAdd" 정보가 전달되는 것으로 봐서 아마도(순전히 추측입니다. 자바를 잘 모르므로!) LambdaMetafactory 타입의 metafactory 메서드에 의해 IAdd 인터페이스가 인스턴스화 되면서 반환되는 것으로 보입니다.

그렇게 반환된 인스턴스는 이후 astore_1 명령으로 첫 번째 로컬변수에 담기고 invokeinterface 명령어의 인자로 전달되기 위해 다시 aload_1 명령어로 스택에 적재되는 것을 볼 수 있습니다.

5: astore_1
6: getstatic     #20                 // Field java/lang/System.out:Ljava/io/PrintStream;
9: aload_1
10: bipush        10
12: bipush        20
14: invokeinterface #26,  3           // InterfaceMethod testapp/Lambda$IAdd.Add:(II)I
19: invokevirtual #31                 // Method java/io/PrintStream.println:(I)V

여기서 한 가지 궁금한 것이 있다면, 왜 자바 언어 개발자들이 성능상 다소 불리한 동적 생성을 통한 방식을 택했냐는 것입니다. 뭐랄까... 이것은 마치 C#에서 다음과 같이 구현하면 되는 것을,

AddDelegate func = (a, b) => a + b;

이보다 더 느린 동적 바인딩을 사용한 것이나 유사합니다.

dynamic func = (a, b) => a + b; // 이 코드는 컴파일되지 않습니다!

엄밀히 따지고 보면, 자바 컴파일러 역시 컴파일 시에 해당 인터페이스를 구현하는 concrete 클래스를 정의할 수 있는 능력이 충분하므로 성능에 더 유리한 정적 바인딩이 가능했을 것입니다. 암튼, 이 부분은 그 의도가 매우 궁금합니다. ^^




전반적으로 보면 자바의 람다는 delegate의 부재에도 불구하고 깨끗하게 잘 구현된 것 같습니다. ^^ 자... 그럼 마지막으로 자바의 람다에서 가장 말이 많은 클로저(closure)의 변수 캡처(Captured Variable)를 알아볼까요?

이미 많은 분들이 아시겠지만 자바의 클로저는 캡처된 변수의 값을 바꾸는 것을 허용하지 않습니다. 즉, 다음과 같은 코드는 컴파일 시에 오류가 발생합니다.

int captured = 100;
IAdd func = (a, b) -> a + b + (captured ++); // 컴파일 에러: Local variable captured defined in an enclosing scope must be final or effectively final

물론 값을 변화시키지 않으면 컴파일이 잘 됩니다.

int captured = 100;
IAdd func = (a, b) -> a + b + captured; // 컴파일 성공

어쩌면 이것이 자바의 람다 구현 방식에서 발생하는 제약이 아닌가 하는 의문을 가질 수 있는데요. 내부를 들여다 보면 그 물음에 답이 나옵니다. 어디... 위의 코드를 역어셈블을 통해 살펴볼까요? ^^

우선, 자바 컴파일러는 캡처된 변수를 invokedynamic 호출에 전달합니다.

0: bipush        100
2: istore_1
3: iload_1
4: invokedynamic #19,  0             // InvokeDynamic #0:Add:(I)Ltestapp/Lambda$IAdd;

그럼, 부트스트랩 메서드인 LambdaMetafactory.metafactory에서는 내부적으로 스택을 통해 전달된 그 변수들을 전달받고는,

  BootstrapMethods:
    0: #56 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
      Method arguments:
        #57 (II)I
        #60 invokestatic testapp/Lambda.lambda$0:(III)I
        #61 (II)I

실행 시 "invokestatic testapp/Lambda.lambda$0:(III)I"을 호출하면서 역시 lambda$0 메서드에 캡처된 변수값을 전달할 것입니다.

  private static int lambda$0(int, int, int);
    descriptor: (III)I
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: iload_0
         4: iadd
         5: ireturn

수수께끼는 여기서 풀립니다. 보는 바와 같이 마지막 세 번째 int 변수 값에 캡처된 변수의 값이 전달되기 때문입니다. 따라서, 코드 내부에서 해당 변수의 값을 바꾼다고 해서 원래의 캡처된 변수의 값에는 아무런 영향을 줄 수가 없습니다.

이에 대한 해명으로 함수형 언어들의 캡처된 변수가 immutable을 지향하므로 자바도 이에 대한 철학을 따랐다고 나오긴 하는데... 글쎄요. 위와 같이 값 형식이 넘어간 경우에는 원본 객체의 값을 변화시킬 수 없지만 참조 형식으로 넘어간 경우라면,

Vector v = new Vector();
IAdd func = (a, b) -> { v.add(5); return a + b; };
System.out.println(func.Add(10, 20));
        
System.out.println(v.size()); // 출력 결과: 1

당연히 객체의 내부 상태가 변합니다. 자바 자체가 전체적으로 mutable이면서 함수형 언어의 철학을 클로저의 구현에서만 고집한다는 것은 그다지 합당한 이유는 아닌 것 같습니다. 결국, 구조적으로 봤을 때 원본 변수의 상태를 바꾸기에는 우회해야 할 것이 너무 많아졌기 때문에 아마도 복잡도를 생각해서 접은 것이 아닌가 생각됩니다.




[내용 업데이트: 2014-06-11]
자바의 람다 인스턴스를 가지고 리플렉션을 해보면 좀 더 확실하게 정체를 알 수 있습니다.

IAdd func = (a, b) -> a + b;
        
Class<?> clazz = func.getClass();
        
System.out.println(clazz.getName()); // == testapp.Lambda$$Lambda$1/1510467688
System.out.println(clazz.getSuperclass().getName()); // == java.lang.Object
        
for (Class<?> cl : clazz.getInterfaces())
{
    System.out.println(cl.getName()); // testapp.Lambda$IAdd
}

/*
Add
wait
wait
wait
equals
toString
hashCode
getClass
notify
notifyAll
*/
for (Method mt : clazz.getMethods())
{
    System.out.println(mt.getName());
}
        
System.out.println(clazz.isSynthetic()); // == true

마지막의 isSynthetic으로 검색해 보면 다음과 같은 질문/답변을 찾을 수 있습니다.

Java has the ability to create classes at runtime. These classes are known as Synthetic Classes or Dynamic Proxies.
; http://stackoverflow.com/questions/399546/synthetic-class-in-java

A class may be marked as synthetic if it is generated by the compiler, that is, it does not appear in the source code.

Dynamic Proxy Classes
; http://docs.oracle.com/javase/1.5.0/docs/guide/reflection/proxy.html

A dynamic proxy class is a class that implements a list of interfaces specified at runtime 

그리고 이규원님이 한가지 더 의견을 주셨네요. ^^

Java의 람다가 C#의 그것을 절대 따라올 수 없는 이유는 ExpressionTree라고 생각합니다.


그렇군요. ^^ C#의 람다는 "코드 또는 데이터로써" 다뤄질 수 있지만, 자바의 람다는 "코드"로써만 다뤄진다는 차이점이 있습니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 8/31/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2014-06-11 12시05분
좋은 분석 감사합니다!
Gyuwon Yi
2014-06-11 01시00분
[김우승] FunctionalInterface 는 C++ 로 이야기하면 compile time 점검을 하는가 link time 점검을 하는가의 차이겠죠.
Override 와 비교해보면 효용성이 얼마나 있는지 의문일 수도 있겠으나
이미 FunctionalInterface 가 지정되어 있는 것을 함부로 추가 operation 을 정의하는 실수는 방지할 수 있을 것으로 보입니다.
[guest]
2014-06-11 05시32분
예전부터 의문 중 하나가 문법적 단순함과 순수함을 유지하기 위해서라지만 그 문법적 단순함을 위해 더 복잡한 코드를 사용하게 만드는 자바가 잘 나가는게 좀 궁금하기도 했었습니다.
인터넷에 널리고 널린 온갖 자바를 위한 라이브러리 프레임워크가 그 이유일까 하는 생각도 들었었구요.
15년 전 학생때 배운 것만 생각할때에는 지금도 잘 나가는 이유가 다른 좋은게 더 나와서이지 않을까 하는 생각만 하고 실제 살펴보진 못하다가 임백준님의 폴리글랏 프로그래밍 이란 책을 보니 언어의 변화는 거의 없었던 것 같아 보이더라구요.
어쩌다 보니 C#만 주구장창 하게 되어가지곤 비교대상이 C#과 자바뿐이었는데 자바 좋아하시는 분들 보시면 제 의견이 좀 짜증날 수도 있지만(^^) 정말 언어적인 측면에서만 보자면 정말 떨어지는 언어처럼 보이는 건 제가 사용하는 언어에 대한 자만만은 아닌것 같습니다.
그래도 현저히 점유율에서 자바에 밀리는 C#을 보면 좀 안타깝기도 하구요.^^
좋은 분석 감사합니다.
Beren Ko
2014-06-11 09시50분
[spowner] 정태님 갑자기 궁금합니다. 왜 굳이 이렇게 분석을 하시지요? ㅎㅎ물론 보는 사람이야 누워서 떡 먹은 기분이라.. 감사하죠;
[guest]
2014-06-11 11시46분
@김우승 네, 맞는 말씀입니다. ^^

@Beren Ko, 그러게요... ^^ 마이크로소프트라는 폐쇄성이 한몫한 것 같지만, 어쨌든 그 덕분에 마이크로소프트의 많은 정책들이 오픈되는 쪽으로 발전했으니 그나마 다행인 것 같습니다.

@spowner, 아... 어떤 분이 살짝 부탁을 하셨습니다. 아마도 예전에 제네릭 관련해서 비교한 글을 보시고 람다가 나오면서 제 생각이 나신 것 같습니다. ^^

자바와 닷넷의 제네릭 차이점 - 중간 언어 및 공변/반공변 처리
; http://www.sysnet.pe.kr/2/0/1581
정성태
2014-06-12 02시02분
"(본문 인용) 코드 내부에서 해당 변수의 값을 바꾼다고 해서 원래의 캡쳐된 변수의 값에는 아무런 영향을 줄 수가 없습니다. 이에 대한 해명으로 함수형 언어들의 캡처된 변수가 immutable을 지향하므로 자바도 이에 대한 철학을 따랐다고 나오긴 하는데... 글쎄요. 위와 같이 값 형식이 넘어간 경우에는 원본 객체의 값을 변화시킬 수 없지만 참조 형식으로 넘어간 경우라면, 당연히 객체의 내부 상태가 변합니다. 자바 자체가 전체적으로 mutable이면서 함수형 언어의 철학을 클로저의 구현에서만 고집한다는 것은 그다지 합당한 이유는 아닌 것 같습니다."

자바에서 왜 final이 아닌 로컬변수를 람다식에서 캡처할 수 없는 이유에 대해서 처음에는 저도 굉장히 의아해했었습니다. 그러나 람다식을 사용하는 입장에서 생각해 본다면, 아래와 같은 연유에서, 자바의 이 방식이 C# 그것보다 더 좋을 수 있겠다는 생각을 하게 되었습니다.

예를 들어, C#에서 람다식을 사용하면서 아래와 같은 오류를 누구나 한 번쯤은 경험했을 것으로 생각합니다. 이 오류는 사실 알고 보면 별것 아니나, 람다식에 대한 이해가 부족한 사람의 경우는 정말 곤욕스러운 오류이지 않을 수 없습니다.

int i = 1;
Action action = () => Debug.Assert(i == 1, "i는 1입니다.");
i = 2;
action();

또한, 저의 경우에 로컬변수 값(또는 객체) 자체가 변경되는 상황(위 예와 같이)에서 람다식을 사용하는 경우가 극히 드물었습니다. 오히려 자바에서와 같이 로컬변수에 readonly를 붙여 위와 같은 오류를 사전에 막는 것이 더 좋다는 생각까지 들었습니다.

저의 개인적인 견해이니, 의문 사항이나 잘못된 부분이 있으면 말씀 주시기 바랍니다.
Jin-Wook Chung
2014-06-13 12시36분
[땡초] int i = 1;
Action action = () => Debug.Assert(i == 1, "i는 1입니다.");
i = 2;
action();

이 코드를 C++로 풀어 쓴다면 당연한 결과라고 생각합니다.
그리고 의도적으로 포인터 함수처럼, 이런 결과를 의도한 코드를 만드는 게 맞는 것 같아요.

그래서 ExpressionTree 에서는 온전한 코드로 컴파일하는 Compile() 메서드를 지원해 주는 것 같습니다.
[guest]
2014-06-13 01시36분
@Jin-Wook Chung 님의 의견에 동의는 합니다. 하지만, 그 문제의 결과가 1이라고 예상하는 사람과 2라고 예상하는 사람이 절반 정도씩 있지 않을까요? ^^

참고로, Javascript 같은 경우에도 결과가 C#과 마찬가지로 출력됩니다. (엄밀히, Javascript의 경우에는 captured 되었다는 의미보다는 context에 의존하는 경우라서 그렇겠지요. ^^)

     var i = 1;
     var func = function() { document.body.innerHTML = i; }
     i = 2;
     func(); // 출력: 2

반면, C++의 경우에는 자바와 같군요. ^^

     int i = 1;
     auto func = [i] { printf("%d", i ); };
     i = 2;
     func(); // 출력: 1

하지만, 이렇게 참조형식으로 전달하는 방식도 지원합니다.

     int i = 1;
     auto func = [&i] { printf("%d", i ); };
     i = 2;
     func(); // 출력: 2

잘 만든 것인지 모르지만, clojure의 경우.

     (def i (atom 1))
     (defn outputFunc [] (println @i))
     (reset! i 2)
     (outputFunc) // 출력: 2

또는,

    (def x (ref 1))
    (defn outputFunc [] (println @x))
    (dosync (alter x inc))
    (outputFunc) // 출력: 2
정성태
2014-06-13 05시53분
[정진욱] @정성태
중복된 댓글이 정리되었네요. 감사합니다. 아울러 좋은 글 포스팅해 주셔서 고맙습니다.

@땡초 @정성태
그럼 이렇게 되나요?

1. 캡처된 변수를 변경할 수 있음: C#, Javascript, Clojure
2. 캡처된 변수를 변경할 수 없음: Java(effectively final)
3. 둘다 가능: C++

사실 위에서 제가 하고 싶었던 말은, '캡처된 변수를 변경하는 일이 얼마나 있을까?'라는 것입니다.
저는 개인적으로 캡처된 변수가 변경이 되는 것을 가정하고 프로그래밍한 경험이 거의 없습니다. 그래서 자바의 effectively final개념이 나쁘지 않다고 생각합니다.

아마 우리는 앞서 예제에서 아마 아래와 같이 프로그래밍한 경험을 종종 해 보셨을 것 같습니다. 아래 코드는 캐처된 변수의 변경을 막기해서 임시 변수에 저장하였다나 람다로 넘기는 것을 보여주고 있습니다.

int i = 1;
int tmp = i;
Action action = () => Debug.Assert(tmp == 1, "i는 1입니다.");
i = 2;
action(); // 1출력
[guest]
2014-06-13 07시29분
[spowner] @정진욱

글쎄요. 쓰기 나름인 것 같은데요. 현재 C# 처럼 동작을 해야 깔끔하게 코딩되는 경우가 되려 많은 것 같습니다. 그리고 말씀하신 아래와 같은 오류는 전 한번도 경험해본 적이 없습니다.
메소드 코드가 길어봤자 얼마나 길겠어요. ^^;
[guest]
2016-04-08 08시06분
[나그네] C#에서 스레드 돌리는게 자바와 비슷했었나요? C#을 만진지 오래되서 (제가 c#을 만질때는 쓰레드 돌릴때 대리자 만들고 해서 돌렸던거 같은데 람다 표현식 도입하면서 모양이 비슷해졌나 보네요)
그리고 자바쪽 스레드 실행이 잘못되었네요.. 그냥 run 하면 스레드를 실행하는게 아니라 스레드에 있는 run 함수를 실행하는거 밖에 안됩니다. 즉, 메인 스레드에서 돌아간다는거죠.. 그냥 run 함수를 불렀으니.. 자바에서도 똑같이 start란 함수가 존제합니다.
[guest]
2016-04-08 08시50분
나그네 님, 의견 감사합니다. run을 start로 수정했습니다. ^^ (자바 못하는 티가 나는군요. ^^;)
정성태

1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13432정성태10/31/20232455오류 유형: 878. 탐색기의 WSL 디렉터리 접근 시 "Attempt to access invalid address." 오류 발생
13431정성태10/31/20232790스크립트: 60. 파이썬 - 비동기 FastAPI 앱을 gunicorn으로 호스팅
13430정성태10/30/20232678닷넷: 2153. C# - 사용자가 빌드한 ICU dll 파일을 사용하는 방법
13429정성태10/27/20232961닷넷: 2152. Win32 Interop - C/C++ DLL로부터 이중 포인터 버퍼를 C#으로 받는 예제파일 다운로드1
13428정성태10/25/20233035닷넷: 2151. C# 12 - ref readonly 매개변수
13427정성태10/18/20233250닷넷: 2150. C# 12 - 정적 문맥에서 인스턴스 멤버에 대한 nameof 접근 허용(Allow nameof to always access instance members from static context)
13426정성태10/13/20233408스크립트: 59. 파이썬 - 비동기 호출 함수(run_until_complete, run_in_executor, create_task, run_in_threadpool)
13425정성태10/11/20233195닷넷: 2149. C# - PLinq의 Partitioner<T>를 이용한 사용자 정의 분할파일 다운로드1
13423정성태10/6/20233173스크립트: 58. 파이썬 - async/await 기본 사용법
13422정성태10/5/20233319닷넷: 2148. C# - async 유무에 따른 awaitable 메서드의 병렬 및 예외 처리
13421정성태10/4/20233395닷넷: 2147. C# - 비동기 메서드의 async 예약어 유무에 따른 차이
13420정성태9/26/20235585스크립트: 57. 파이썬 - UnboundLocalError: cannot access local variable '...' where it is not associated with a value
13419정성태9/25/20233220스크립트: 56. 파이썬 - RuntimeError: dictionary changed size during iteration
13418정성태9/25/20233925닷넷: 2146. C# - ConcurrentDictionary 자료 구조의 동기화 방식
13417정성태9/19/20233454닷넷: 2145. C# - 제네릭의 형식 매개변수에 속한 (매개변수를 가진) 생성자를 호출하는 방법
13416정성태9/19/20233259오류 유형: 877. redis-py - MISCONF Redis is configured to save RDB snapshots, ...
13415정성태9/18/20233756닷넷: 2144. C# 12 - 컬렉션 식(Collection Expressions)
13414정성태9/16/20233514디버깅 기술: 193. Windbg - ThreadStatic 필드 값을 조사하는 방법
13413정성태9/14/20233710닷넷: 2143. C# - 시스템 Time Zone 변경 시 이벤트 알림을 받는 방법
13412정성태9/14/20236992닷넷: 2142. C# 12 - 인라인 배열(Inline Arrays) [1]
13411정성태9/12/20233493Windows: 252. 권한 상승 전/후 따로 관리되는 공유 네트워크 드라이브 정보
13410정성태9/11/20235031닷넷: 2141. C# 12 - Interceptor (컴파일 시에 메서드 호출 재작성) [1]
13409정성태9/8/20233888닷넷: 2140. C# - Win32 API를 이용한 모니터 전원 끄기
13408정성태9/5/20233843Windows: 251. 임의로 만든 EXE 파일을 포함한 ZIP 파일의 압축을 해제할 때 Windows Defender에 의해 삭제되는 경우
13407정성태9/4/20233588닷넷: 2139. C# - ParallelEnumerable을 이용한 IEnumerable에 대한 병렬 처리
13406정성태9/4/20233557VS.NET IDE: 186. Visual Studio Community 버전의 라이선스
1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...